From c49ff9f88aca975467e03c3ba6ebda709841f127 Mon Sep 17 00:00:00 2001 From: Stephen Compall Date: Tue, 7 Jul 2026 21:17:25 +0000 Subject: [PATCH 01/13] Undo our changes Signed-off-by: Stephen Compall --- canton/UNRELEASED.md | 3 + .../test/resources/gen-test-certificates.sh | 2 +- canton/canton-json.lnav.json | 146 +- .../synchronizer_connectivity_service.proto | 34 +- .../canton/admin/topology/v30/common.proto | 20 - .../admin/api/client/GrpcCtlRunner.scala | 21 +- .../api/client/commands/AdminCommand.scala | 8 +- .../client/commands/LedgerApiCommands.scala | 2 +- .../commands/ParticipantAdminCommands.scala | 155 +- .../commands/SequencerAdminCommands.scala | 25 +- .../commands/SequencerBftAdminCommands.scala | 29 - .../commands/TopologyAdminCommands.scala | 18 +- .../client/data/RegisteredSynchronizer.scala | 107 - .../client/data/SequencerConnections.scala | 55 +- .../client/data/SequencingParameters.scala | 2 +- .../data/SynchronizerConnectionConfig.scala | 8 +- .../client/data/SynchronizerParameters.scala | 2 +- .../admin/api/client/data/Topology.scala | 29 - .../canton/config/CantonConfig.scala | 170 +- .../canton/console/AdminCommandRunner.scala | 6 +- .../canton/console/ConsoleEnvironment.scala | 37 +- .../console/GrpcAdminCommandRunner.scala | 14 +- .../canton/console/HealthDumpGenerator.scala | 4 +- .../canton/console/InstanceReference.scala | 30 +- .../ParticipantReferencesExtensions.scala | 3 - ...ExternalPartiesTestingAdministration.scala | 6 +- .../commands/LedgerApiAdministration.scala | 10 +- .../commands/ParticipantAdministration.scala | 36 +- .../ParticipantRepairAdministration.scala | 44 +- .../commands/PartiesAdministration.scala | 3 +- .../commands/SequencerAdministration.scala | 2 +- .../TopologyAdministrationGroup.scala | 51 +- .../declarative/DeclarativeApiManager.scala | 1 - .../DeclarativeParticipantApi.scala | 48 +- .../CommunityEnvironmentFactory.scala | 6 +- .../canton/environment/Environment.scala | 56 +- .../canton/environment/Nodes.scala | 8 +- .../canton/environment/package.scala | 10 - .../canton/metrics/MetricsRegistry.scala | 5 +- .../app/src/main/resources/logback.xml | 12 +- .../app/src/main/resources/repl/banner.txt | 14 +- .../digitalasset/canton/CantonAppDriver.scala | 29 +- .../canton/CantonCommunityApp.scala | 20 +- .../com/digitalasset/canton/Runner.scala | 18 +- .../digitalasset/canton/config/Generate.scala | 2 +- .../examples/01-simple-topology/README.md | 2 +- .../run-docker-postgres.sh | 2 +- .../pack/examples/05-composability/README.md | 4 +- .../app/src/pack/examples/07-repair/README.md | 29 +- .../Canton/bft-ordering-performance.json | 1777 ++++++----------- .../dashboards/Canton/bft-ordering.json | 1425 +++++-------- .../admin/api/client/GrpcCtlRunnerTest.scala | 1 - .../canton/console/ConsoleTest.scala | 4 +- .../examples/ExampleIntegrationTest.scala | 2 +- .../MultisyncExampleIntegrationTest.scala | 2 +- .../jsonapi/HttpServiceUserFixture.scala | 2 +- ...eSubmissionVersioningIntegrationTest.scala | 3 +- .../BasePerformanceIntegrationTest.scala | 2 + ...dWithSequencerRestartIntegrationTest.scala | 22 +- ...izerPerformanceRunnerIntegrationTest.scala | 1 + ...ggregationPerformanceIntegrationTest.scala | 78 +- .../tests/release/CliIntegrationTest.scala | 39 +- .../RehydrationIntegrationTest.scala | 3 +- .../lsu/LsuEndToEndIntegrationTest.scala | 94 - .../util/PartyToParticipantDeclarative.scala | 6 +- .../canton/topology/admin/v30/common.proto | 12 - .../canton/config/LoggingConfig.scala | 3 +- .../config/RefinedNonNegativeDuration.scala | 12 +- .../canton/config/StorageConfig.scala | 2 +- .../digitalasset/canton/crypto/Signing.scala | 6 +- .../crypto/provider/jce/JcePureCrypto.scala | 2 +- .../canton/data/CantonTimestamp.scala | 22 - .../canton/lifecycle/HasLifeCycleScope.scala | 6 +- .../canton/lifecycle/OnShutdownRunner.scala | 41 +- .../lifecycle/PerformUnlessClosing.scala | 5 - .../logging/pretty/PrettyInstances.scala | 17 +- .../protocol/SynchronizerParameters.scala | 4 +- .../messages/ConfirmationResultMessage.scala | 2 +- .../protocol/messages/ProtocolMessage.scala | 8 +- .../messages/SetTrafficPurchasedMessage.scala | 2 +- .../canton/resource/DbMigrations.scala | 1 + .../canton/resource/DbVersionCheck.scala | 2 +- .../client/SequencerClientFactory.scala | 3 +- .../TrafficPurchasedSubmissionHandler.scala | 2 +- .../digitalasset/canton/topology/Member.scala | 3 - .../topology/TopologyManagerError.scala | 27 - .../TopologyStateWriteThroughCache.scala | 7 +- .../store/TopologyTransactionCollection.scala | 2 +- .../topology/store/db/DbTopologyStore.scala | 11 +- .../store/memory/InMemoryTopologyStore.scala | 10 +- .../digitalasset/canton/util/ErrorUtil.scala | 7 - .../digitalasset/canton/util/PekkoUtil.scala | 13 +- .../canton/util/retry/Policy.scala | 30 +- .../canton/data/ConcurrentHMap.scala | 4 +- .../topology/admin/grpc/TopologyStoreId.scala | 2 +- .../canton/util/GrpcStreamingUtils.scala | 37 - .../protocol/ExampleTransactionFactory.scala | 24 +- .../digitalasset/canton/store/db/DbTest.scala | 10 +- .../traffic/TrafficControlProcessorTest.scala | 2 +- .../BaseEnvironmentDefinition.scala | 44 - .../integration/BaseIntegrationTest.scala | 33 +- .../CommunityIntegrationTest.scala | 11 +- .../ConcurrentEnvironmentLimiter.scala | 2 +- .../canton/integration/ConfigTransforms.scala | 2 +- .../integration/EnvironmentDefinition.scala | 94 +- .../canton/integration/EnvironmentSetup.scala | 322 +-- .../integration/EnvironmentSetupPlugin.scala | 14 +- .../LedgerApiStoreIntegrityChecker.scala | 9 +- .../canton/integration/TestEnvironment.scala | 11 +- .../bootstrap/NetworkBootstrapper.scala | 4 +- .../canton/integration/package.scala | 16 +- .../integration/plugins/UseBftSequencer.scala | 1 - .../canton/integration/plugins/UseOtlp.scala | 2 +- .../plugins/UseReferenceBlockSequencer.scala | 2 +- .../plugins/tinyproxy/UseTinyProxy.scala | 4 +- .../integration/util/AcsInspection.scala | 22 +- .../integration/util/EntitySyntax.scala | 8 +- .../ProgrammableSequencerPolicies.scala | 2 +- .../tool/src/main/resources/logback.xml | 23 - .../ledger/api/auth/RequiredClaims.scala | 16 +- .../canton/ledger/client/LedgerClient.scala | 2 +- .../error/groups/CommandExecutionErrors.scala | 13 +- .../StoreBackedCommandInterpreter.scala | 18 +- .../apiserver/ratelimiting/MemoryCheck.scala | 8 +- .../platform/index/IndexServiceImpl.scala | 6 +- .../ledger/api/IdentityProviderIdSpec.scala | 2 +- .../localstore/PartyRecordStoreTests.scala | 2 + .../ledger/localstore/UserStoreTests.scala | 13 - .../PreparedTransactionCodecV1Spec.scala | 34 - .../lf/value/json/ApiCodecCompressed.scala | 371 ---- .../lf/value/json/ApiValueImplicits.scala | 41 - .../daml/lf/value/json/JsonVariant.scala | 21 - .../lf/value/json/NavigatorModelAliases.scala | 68 - .../canton/http/json/v2/Endpoints.scala | 6 +- .../canton/http/json/v2/V2Routes.scala | 6 +- .../crypto/digests/canton/Blake2bDigest.java | 8 +- canton/community/lib/README.md | 11 +- .../scala/slick/jdbc/canton/StaticQuery.scala | 5 - .../canton/DirectGrpcServiceInvocation.scala | 4 +- .../canton/EnforceVisibleForTesting.scala | 6 +- .../canton/RequireBlockingTest.scala | 1 + .../daml.yaml | 10 +- .../daml.yaml | 10 +- .../grpc/GrpcPartyManagementService.scala | 3 +- .../GrpcSynchronizerConnectivityService.scala | 35 +- .../admin/party/PartyReplicator.scala | 20 +- .../AssignmentValidationResult.scala | 4 +- .../ReassignmentCoordination.scala | 15 +- .../EncryptedViewMessageFactory.scala | 2 +- .../LegacyTransactionTreeFactory.scala | 2 +- .../pruning/CommitmentCommandsHelper.scala | 2 +- .../FirstUnsafeOffsetComputation.scala | 5 +- .../store/db/DbActiveContractStore.scala | 3 +- .../participant/topology/TopologyLookup.scala | 16 +- .../canton/participant/util/DAMLe.scala | 16 +- .../topology/TopologyLookupTest.scala | 104 - ...sequencer_bft_administration_service.proto | 76 +- .../block/update/BlockChunkProcessor.scala | 5 +- .../block/BlockSequencerThroughputCap.scala | 2 +- .../BftOrderingSequencerAdminService.scala | 14 - .../canton/sequencing/BftBlockOrderer.scala | 2 +- .../p2p/grpc/P2PGrpcConnectionManager.scala | 22 +- .../p2p/grpc/P2PGrpcConnectionState.scala | 4 +- .../modules/consensus/iss/SegmentState.scala | 8 +- .../modules/p2p/P2PNetworkOutModule.scala | 14 +- .../modules/p2p/data/P2PEndpointsStore.scala | 2 +- .../p2p/data/db/DbP2PEndpointsStore.scala | 2 +- .../memory/InMemoryP2PEndpointsStore.scala | 2 +- .../framework/modules/P2PNetworkOut.scala | 1 - ...BftOrderingSequencerAdminServiceTest.scala | 40 +- .../modules/p2p/P2PNetworkOutModuleTest.scala | 34 +- .../p2p/data/P2PEndpointsStoreTest.scala | 16 +- .../performance/BftBenchmark.scala | 53 +- .../performance/BftBenchmarkConfig.scala | 8 +- .../performance/BftBenchmarkTool.scala | 6 +- .../bftordering/performance/BftBinding.scala | 10 +- .../performance/GenStandaloneConfig.scala | 2 + .../dabft/DaBftBenchmarkBinding.scala | 117 +- .../dabft/DaBftBenchmarkTool.scala | 3 +- .../ShortCircuitBindingSpec.scala | 4 +- .../ShortcircuitBenchmarkTool.scala | 5 +- .../shortcircuit/ShortcircuitBinding.scala | 9 +- .../com/digitalasset/canton/BaseTest.scala | 40 +- .../com/digitalasset/canton/LogReporter.scala | 7 +- .../canton/store/db/DbStorageSetup.scala | 3 +- .../canton/tracing/HeaderName.scala | 21 - .../canton/tracing/TraceContextGrpc.scala | 50 +- .../canton/tracing/W3CTraceContext.scala | 32 +- .../src/test/resources/logback-test.xml | 63 - 189 files changed, 2399 insertions(+), 4963 deletions(-) delete mode 100644 canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto delete mode 100644 canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala delete mode 100644 canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala delete mode 100644 canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala delete mode 100644 canton/community/ledger-test-tool/tool/src/main/resources/logback.xml delete mode 100644 canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala delete mode 100644 canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala delete mode 100644 canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala delete mode 100644 canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala delete mode 100644 canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala delete mode 100644 canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/HeaderName.scala delete mode 100644 canton/community/util-observability/src/test/resources/logback-test.xml diff --git a/canton/UNRELEASED.md b/canton/UNRELEASED.md index 41efb3649e..4dbc1f4165 100644 --- a/canton/UNRELEASED.md +++ b/canton/UNRELEASED.md @@ -848,3 +848,6 @@ Canton has been tested against the following versions of its dependencies: |----------------------------|----------------------------| | Java Runtime | JAVA_VERSION | | Postgres | POSTGRES_VERSION | + + + diff --git a/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh b/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh index 62ce00d6d5..fb197768d3 100755 --- a/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh +++ b/canton/base/daml-tls/src/test/resources/gen-test-certificates.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 set -eoux pipefail diff --git a/canton/canton-json.lnav.json b/canton/canton-json.lnav.json index 026404cbac..f6769d6409 100644 --- a/canton/canton-json.lnav.json +++ b/canton/canton-json.lnav.json @@ -1,78 +1,78 @@ { - "$schema": "https://lnav.org/schemas/format-v1.schema.json", - "canton_logstash_json": { - "title": "Canton JSON Log", - "url": "https://github.com/logstash/logstash-logback-encoder", - "description": "Canton JSON log files written using logstash.", - "json": true, - "hide-extra": false, - "file-pattern": "(\\.clog.*)|(docker.*(canton|splice).*)", - "line-format": [ { "field" : "@timestamp" }, " [", { "field" : "thread_name", "max-width" : 20 },"] ", { "field": "level" }, " - ", { "field": "logger_name" }, " (", { "field" : "trace-id", "default-value" : "" }, "-", { "field" : "span-name", "default-value" : "" }, "-", { "field" : "span-parent-id", "default-value" : "" }, "-", { "field" : "span-id", "default-value" : "" }, ") - ", { "field" : "message" }, - " ", - { "field" : "stack_trace", "default-value" : "" } - ], - "timestamp-field" : "@timestamp", - "body-field" : "message", - "level-field" : "level", - "level" : { - "trace" : "TRACE", - "debug" : "DEBUG", - "info" : "INFO", - "error" : "ERROR", - "warning" : "WARN" - }, - "opid-field" : "trace-id", - "opid": { - "subid": "span-id", - "sub-description": { - "span-name": { - "format": [ - { - "field": "span-name" - } - ] - } - } - }, - "value" : { - "logger_name" : { - "kind" : "string", - "identifier" : true - }, - "ipaddress" : { - "kind" : "string", - "identifier" : true - }, - "level_value" : { - "hidden": true - }, - "stack_trace" : { - "kind" : "string" - }, - "trace-id" : { - "kind" : "string", - "identifier" : true - }, - "span-id" : { - "kind" : "string", - "identifier" : true - }, - "span-name" : { - "kind" : "string", - "identifier" : true - }, - "span-parent-id" : { - "kind" : "string", - "identifier" : true - }, - "thread_name" : { - "kind" : "string", - "identifier" : true, - "hidden" : true - }, - "@version" : { - "hidden" : true + "$schema": "https://lnav.org/schemas/format-v1.schema.json", + "canton_logstash_json": { + "title": "Canton JSON Log", + "url": "https://github.com/logstash/logstash-logback-encoder", + "description": "Canton JSON log files written using logstash.", + "json": true, + "hide-extra": false, + "file-pattern": "\\.clog.*", + "line-format": [ { "field" : "@timestamp" }, " [", { "field" : "thread_name", "max-width" : 20 },"] ", { "field": "level" }, " - ", { "field": "logger_name" }, " (", { "field" : "trace-id", "default-value" : "" }, "-", { "field" : "span-name", "default-value" : "" }, "-", { "field" : "span-parent-id", "default-value" : "" }, "-", { "field" : "span-id", "default-value" : "" }, ") - ", { "field" : "message" }, + " ", + { "field" : "stack_trace", "default-value" : "" } + ], + "timestamp-field" : "@timestamp", + "body-field" : "message", + "level-field" : "level", + "level" : { + "trace" : "TRACE", + "debug" : "DEBUG", + "info" : "INFO", + "error" : "ERROR", + "warning" : "WARN" + }, + "opid-field" : "trace-id", + "opid": { + "subid": "span-id", + "sub-description": { + "span-name": { + "format": [ + { + "field": "span-name" } + ] } + } + }, + "value" : { + "logger_name" : { + "kind" : "string", + "identifier" : true + }, + "ipaddress" : { + "kind" : "string", + "identifier" : true + }, + "level_value" : { + "hidden": true + }, + "stack_trace" : { + "kind" : "string" + }, + "trace-id" : { + "kind" : "string", + "identifier" : true + }, + "span-id" : { + "kind" : "string", + "identifier" : true + }, + "span-name" : { + "kind" : "string", + "identifier" : true + }, + "span-parent-id" : { + "kind" : "string", + "identifier" : true + }, + "thread_name" : { + "kind" : "string", + "identifier" : true, + "hidden" : true + }, + "@version" : { + "hidden" : true + } } + } } diff --git a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto index ded3845a12..eba2bad55c 100644 --- a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto +++ b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto @@ -7,7 +7,6 @@ package com.digitalasset.canton.admin.participant.v30; import "com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.proto"; import "com/digitalasset/canton/admin/time/v30/time_tracker_config.proto"; -import "com/digitalasset/canton/admin/topology/v30/common.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; @@ -124,44 +123,13 @@ message ModifySynchronizerRequest { message ModifySynchronizerResponse {} -message ListRegisteredSynchronizersRequest { - // If false (default) returns only active synchronizers. Otherwise, return all synchronizers. - bool all_statuses = 1; -} +message ListRegisteredSynchronizersRequest {} message ListRegisteredSynchronizersResponse { - enum Status { - STATUS_UNSPECIFIED = 0; - - // The node can connect to the synchronizer. - // At most one connection per alias/synchronizer id can be active. - STATUS_ACTIVE = 1; - - // Source and target synchronizer in the context of hard domain migration. - // The node will refuse to connect to a synchronizer with these statuses. - STATUS_HARD_MIGRATING_SOURCE = 2; - STATUS_HARD_MIGRATING_TARGET = 3; - - // Around LSU, the connection of the successor is initially stored with status LSU target. - // When upgrade time is reached and the participant node performs the LSU, then: - // 1. The status of the current synchronizer is changed from Active to LSU source. - // 2. The status of the successor is changes from LSU target to Active. - // The node will refuse to connect to a synchronizer with these statuses. - STATUS_LSU_SOURCE = 4; - STATUS_LSU_TARGET = 5; - - // Currently only used in the context of hard domain migration. - // The old synchronizer connection is marked as Inactive at the end of the migration. - // The node will refuse to connect to a synchronizer with this status. - STATUS_INACTIVE = 6; - } - message Result { SynchronizerConnectionConfig config = 1; bool connected = 2; optional string physical_synchronizer_id = 3; - Status status = 4; - optional com.digitalasset.canton.admin.topology.v30.SynchronizerPredecessor synchronizer_predecessor = 5; } repeated Result results = 1; } diff --git a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto deleted file mode 100644 index 67bf0b5735..0000000000 --- a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -syntax = "proto3"; - -package com.digitalasset.canton.admin.topology.v30; - -import "google/protobuf/timestamp.proto"; - -message SynchronizerPredecessor { - // Id of the predecessor. - string predecessor_physical_id = 1; - - // When the upgrade happened/is supposed to happen. - google.protobuf.Timestamp upgrade_time = 2; - - // Whether the participant is performing a late synchronizer upgrade. - // default: false - bool is_late_upgrade = 3; -} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala index 0c1073c8e4..4f6c57fad6 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunner.scala @@ -7,12 +7,7 @@ import cats.data.EitherT import com.daml.grpc.AuthCallCredentials import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.networking.grpc.{ - CantonGrpcUtil, - GrpcClient, - GrpcError, - GrpcManagedChannel, -} +import com.digitalasset.canton.networking.grpc.{CantonGrpcUtil, GrpcClient, GrpcManagedChannel} import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc} import com.digitalasset.canton.util.LoggerUtil import io.grpc.ManagedChannel @@ -39,7 +34,6 @@ class GrpcCtlRunner( managedChannel: GrpcManagedChannel, token: Option[String], timeout: Duration, - retryPolicy: GrpcError => Boolean, )(implicit ec: ExecutionContext, traceContext: TraceContext): EitherT[Future, String, Result] = { def service(channel: ManagedChannel): command.Svc = { @@ -52,20 +46,15 @@ class GrpcCtlRunner( for { request <- EitherT.fromEither[Future](command.createRequestInternal()) - response <- submitRequest(command)(instanceName, client, request, timeout, retryPolicy) + response <- submitRequest(command)(instanceName, client, request, timeout) result <- EitherT.fromEither[Future](command.handleResponseInternal(response)) } yield result } private def submitRequest[Svc <: AbstractStub[Svc], Req, Res, Result]( command: GrpcAdminCommand[Req, Res, Result] - )( - instanceName: String, - service: GrpcClient[command.Svc], - request: Req, - timeout: Duration, - retryPolicy: GrpcError => Boolean, - )(implicit + )(instanceName: String, service: GrpcClient[command.Svc], request: Req, timeout: Duration)( + implicit ec: ExecutionContext, traceContext: TraceContext, ): EitherT[Future, String, Res] = CantonGrpcUtil.shutdownAsGrpcErrorE( @@ -78,7 +67,7 @@ class GrpcCtlRunner( timeout, logger, CantonGrpcUtil.SilentLogPolicy, // silent log policy, as the ConsoleEnvironment will log the result - retryPolicy, + _ => false, // no retry to optimize for low latency ) .leftMap(_.toString) ) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/AdminCommand.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/AdminCommand.scala index 5faa7fdf9b..f9370a70e7 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/AdminCommand.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/AdminCommand.scala @@ -25,13 +25,13 @@ trait AdminCommand[Req, Res, Result] { */ protected def createRequest(): Either[String, Req] - @inline final def createRequestInternal(): Either[String, Req] = createRequest() + @inline private[client] final def createRequestInternal(): Either[String, Req] = createRequest() /** Handle the response the service has provided */ protected def handleResponse(response: Res): Either[String, Result] - @inline final def handleResponseInternal(response: Res): Either[String, Result] = + @inline private[client] final def handleResponseInternal(response: Res): Either[String, Result] = handleResponse(response) /** Determines within which time frame the request should complete @@ -59,7 +59,7 @@ trait GrpcAdminCommand[Req, Res, Result] extends AdminCommand[Req, Res, Result] */ protected def createService(channel: ManagedChannel): Svc - @inline def createServiceInternal(channel: ManagedChannel): Svc = + @inline private[client] def createServiceInternal(channel: ManagedChannel): Svc = createService(channel) /** Submit the created request to our service @@ -69,7 +69,7 @@ trait GrpcAdminCommand[Req, Res, Result] extends AdminCommand[Req, Res, Result] @GrpcServiceInvocationMethod protected def submitRequest(service: Svc, request: Req): Future[Res] - @inline final def submitRequestInternal(service: Svc, request: Req): Future[Res] = + @inline private[client] final def submitRequestInternal(service: Svc, request: Req): Future[Res] = submitRequest(service, request) } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala index 4ec8a5b3f6..43e4cc7e53 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala @@ -38,13 +38,13 @@ import com.daml.ledger.api.v2.admin.user_management_service.{ ListUsersResponse, RevokeUserRightsRequest, RevokeUserRightsResponse, + Right as UserRight, UpdateUserIdentityProviderIdRequest, UpdateUserIdentityProviderIdResponse, UpdateUserRequest, UpdateUserResponse, User, UserManagementServiceGrpc, - Right as UserRight, } import com.daml.ledger.api.v2.command_completion_service.CommandCompletionServiceGrpc.CommandCompletionServiceStub import com.daml.ledger.api.v2.command_completion_service.{ diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala index 59e185bb00..1c1b6208f9 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala @@ -13,12 +13,6 @@ import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{ } import com.digitalasset.canton.admin.api.client.data as admin import com.digitalasset.canton.admin.api.client.data.PackageDescription.PackageContents -import com.digitalasset.canton.admin.api.client.data.{ - ConfiguredPhysicalSynchronizerId, - RegisteredSynchronizer, - SynchronizerConnectionConfig, - SynchronizerPredecessor, -} import com.digitalasset.canton.admin.participant.v30 import com.digitalasset.canton.admin.participant.v30.PackageServiceGrpc.PackageServiceStub import com.digitalasset.canton.admin.participant.v30.ParticipantInspectionServiceGrpc.ParticipantInspectionServiceStub @@ -54,7 +48,7 @@ import com.digitalasset.canton.participant.pruning.AcsCommitmentProcessor.{ ReceivedCmtState, SentCmtState, } -import com.digitalasset.canton.participant.synchronizer.SynchronizerConnectionConfig as InternalSynchronizerConnectionConfig +import com.digitalasset.canton.participant.synchronizer.SynchronizerConnectionConfig import com.digitalasset.canton.protocol.LfContractId import com.digitalasset.canton.protocol.messages.{AcsCommitment, CommitmentPeriod} import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState @@ -64,11 +58,11 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.time.PositiveSeconds import com.digitalasset.canton.topology.transaction.{GrpcConnection, ParticipantPermission} import com.digitalasset.canton.topology.{ + ConfiguredPhysicalSynchronizerId, ParticipantId, PartyId, PhysicalSynchronizerId, SequencerId, - Synchronizer, SynchronizerId, } import com.digitalasset.canton.tracing.TraceContext @@ -195,13 +189,7 @@ object ParticipantAdminCommands { } } - final case class DarData( - darPath: String, - description: String, - expectedMainPackageId: String, - // We sometimes want to upload DARs that are inside JARs, which is hard with just a path. - darDataO: Option[ByteString] = None, - ) + final case class DarData(darPath: String, description: String, expectedMainPackageId: String) final case class UploadDar( dars: Seq[DarData], synchronizerId: Option[SynchronizerId], @@ -215,17 +203,12 @@ object ParticipantAdminCommands { .traverse(dar => for { _ <- Either.cond(dar.darPath.nonEmpty, (), "Provided DAR path is empty") - filenameAndDarData <- dar.darDataO.fold(loadDarData(dar.darPath))(darData => - Right(Paths.get(dar.darPath).getFileName.toString -> darData) - ) + filenameAndDarData <- loadDarData(dar.darPath) (filename, darData) = filenameAndDarData descriptionOrFilename = if (dar.description.isEmpty) PathUtils.getFilenameWithoutExtension(Path.of(filename)) else dar.description - _ = logger.info(s"Sending upload dar for ${descriptionOrFilename}")( - TraceContext.empty - ) } yield v30.UploadDarRequest.UploadDarData( darData, Some(descriptionOrFilename), @@ -289,9 +272,8 @@ object ParticipantAdminCommands { expectedMainPackageId: String, requestHeaders: Map[String, String], logger: TracedLogger, - darDataO: Option[ByteString], ): UploadDar = UploadDar( - Seq(DarData(darPath, description, expectedMainPackageId, darDataO)), + Seq(DarData(darPath, description, expectedMainPackageId)), synchronizerId, vetAllPackages = vetAllPackages, synchronizeVetting = synchronizeVetting, @@ -730,8 +712,8 @@ object ParticipantAdminCommands { } final case class ImportPartyAcs( - inputStream: java.io.InputStream, - synchronizer: Synchronizer, + file: File, + synchronizerId: SynchronizerId, workflowIdPrefix: String, contractImportMode: ContractImportMode, representativePackageIdOverride: RepresentativePackageIdOverride, @@ -755,7 +737,7 @@ object ParticipantAdminCommands { service: PartyManagementServiceStub, request: Unit, ): Future[v30.ImportPartyAcsResponse] = - ResourceUtil.withResource(inputStream) { inputStream => + ResourceUtil.withResource(new FileInputStream(file)) { inputStream => val isFirstChunk = new AtomicBoolean(true) GrpcStreamingUtils.streamToServer( service.importPartyAcs, @@ -763,7 +745,7 @@ object ParticipantAdminCommands { val isFirst = isFirstChunk.getAndSet(false) v30.ImportPartyAcsRequest( ByteString.copyFrom(bytes), - synchronizerId = Option.when(isFirst)(synchronizer.toProtoPrimitive), + synchronizerId = Option.when(isFirst)(synchronizerId.toProtoPrimitive), workflowIdPrefix = if (isFirst) OptionUtil.emptyStringAsNone(workflowIdPrefix) else None, contractImportMode = Option.when(isFirst)(contractImportMode.toProtoV30), @@ -922,51 +904,6 @@ object ParticipantAdminCommands { ): Either[String, Unit] = Right(()) } - final case class ImportAcsBytes( - acsChunk: Seq[ByteString], - workflowIdPrefix: String, - contractImportMode: ContractImportMode, - representativePackageIdOverride: RepresentativePackageIdOverride, - excludedStakeholders: Set[PartyId], - synchronizerId: SynchronizerId, - ) extends GrpcAdminCommand[ - Unit, - v30.ImportAcsResponse, - Unit, - ] { - - override type Svc = ParticipantRepairServiceStub - - override def createService(channel: ManagedChannel): ParticipantRepairServiceStub = - v30.ParticipantRepairServiceGrpc.stub(channel) - - override protected def createRequest(): Either[String, Unit] = - Right(()) - - override protected def submitRequest( - service: ParticipantRepairServiceStub, - request: Unit, - ): Future[v30.ImportAcsResponse] = { - GrpcStreamingUtils.streamToServerChunked( - service.importAcs, - acsChunk.map(chunk => - v30.ImportAcsRequest( - chunk, - Some(workflowIdPrefix), - Some(contractImportMode.toProtoV30), - excludedStakeholders.map(_.toProtoPrimitive).toSeq, - Some(representativePackageIdOverride.toProtoV30), - Some(synchronizerId.toProtoPrimitive), - ) - ), - ) - } - - override protected def handleResponse( - response: v30.ImportAcsResponse - ): Either[String, Unit] = Right(()) - } - final case class PurgeContracts( synchronizerAlias: SynchronizerAlias, contracts: Seq[LfContractId], @@ -1000,7 +937,7 @@ object ParticipantAdminCommands { final case class MigrateSynchronizer( sourceSynchronizerAlias: SynchronizerAlias, - targetSynchronizerConfig: InternalSynchronizerConnectionConfig, + targetSynchronizerConfig: SynchronizerConnectionConfig, force: Boolean, ) extends GrpcAdminCommand[ v30.MigrateSynchronizerRequest, @@ -1212,7 +1149,7 @@ object ParticipantAdminCommands { currentPsid: PhysicalSynchronizerId, successorPsid: PhysicalSynchronizerId, upgradeTime: CantonTimestamp, - successorConfig: InternalSynchronizerConnectionConfig, + successorConfig: SynchronizerConnectionConfig, sequencerConnectionValidation: SequencerConnectionValidation, ) extends GrpcAdminCommand[ v30.PerformLateLsuRequest, @@ -1446,7 +1383,7 @@ object ParticipantAdminCommands { } - final case object ListActiveRegisteredSynchronizers + final case object ListRegisteredSynchronizers extends Base[ v30.ListRegisteredSynchronizersRequest, v30.ListRegisteredSynchronizersResponse, @@ -1458,7 +1395,7 @@ object ParticipantAdminCommands { override protected def createRequest() : Either[String, v30.ListRegisteredSynchronizersRequest] = Right( - v30.ListRegisteredSynchronizersRequest(allStatuses = false) + v30.ListRegisteredSynchronizersRequest() ) override protected def submitRequest( @@ -1481,75 +1418,21 @@ object ParticipantAdminCommands { ] = for { configP <- result.config.toRight("Server has sent empty config") - config <- InternalSynchronizerConnectionConfig.fromProtoV30(configP).leftMap(_.toString) + config <- SynchronizerConnectionConfig.fromProtoV30(configP).leftMap(_.toString) psid <- result.physicalSynchronizerId .traverse( PhysicalSynchronizerId.fromProtoPrimitive(_, "physical_synchronizer_id") ) .map(ConfiguredPhysicalSynchronizerId(_)) .leftMap(_.toString) - } yield (SynchronizerConnectionConfig.fromInternal(config), psid, result.connected) - - response.results.traverse(mapRes) - } - } - - // All registered synchronziers (including inactive ones) - final case object ListAllRegisteredSynchronizers - extends Base[ - v30.ListRegisteredSynchronizersRequest, - v30.ListRegisteredSynchronizersResponse, - Seq[RegisteredSynchronizer], - ] { - - override protected def createRequest() - : Either[String, v30.ListRegisteredSynchronizersRequest] = - Right( - v30.ListRegisteredSynchronizersRequest(allStatuses = true) - ) - - override protected def submitRequest( - service: SynchronizerConnectivityServiceStub, - request: v30.ListRegisteredSynchronizersRequest, - ): Future[v30.ListRegisteredSynchronizersResponse] = - service.listRegisteredSynchronizers(request) - - override protected def handleResponse( - response: v30.ListRegisteredSynchronizersResponse - ): Either[String, Seq[RegisteredSynchronizer]] = { - - def mapRes( - result: v30.ListRegisteredSynchronizersResponse.Result - ): Either[String, RegisteredSynchronizer] = - for { - configP <- result.config.toRight("Server has sent empty config") - config <- InternalSynchronizerConnectionConfig.fromProtoV30(configP).leftMap(_.toString) - psid <- result.physicalSynchronizerId - .traverse( - PhysicalSynchronizerId.fromProtoPrimitive(_, "physical_synchronizer_id") - ) - .map(ConfiguredPhysicalSynchronizerId(_)) - .leftMap(_.toString) - - predecessor <- result.synchronizerPredecessor - .traverse(SynchronizerPredecessor.fromProtoV30) - .leftMap(_.toString) - - status <- RegisteredSynchronizer.Status.fromProtoV30(result.status).leftMap(_.toString) - } yield RegisteredSynchronizer( - config = SynchronizerConnectionConfig.fromInternal(config), - status = status, - psid = psid, - predecessor = predecessor, - isConnected = result.connected, - ) + } yield (config, psid, result.connected) response.results.traverse(mapRes) } } final case class ConnectSynchronizer( - config: InternalSynchronizerConnectionConfig, + config: SynchronizerConnectionConfig, sequencerConnectionValidation: SequencerConnectionValidation, ) extends Base[v30.ConnectSynchronizerRequest, v30.ConnectSynchronizerResponse, Unit] { @@ -1577,7 +1460,7 @@ object ParticipantAdminCommands { } final case class RegisterSynchronizer( - config: InternalSynchronizerConnectionConfig, + config: SynchronizerConnectionConfig, performHandshake: Boolean, sequencerConnectionValidation: SequencerConnectionValidation, ) extends Base[v30.RegisterSynchronizerRequest, v30.RegisterSynchronizerResponse, Unit] { @@ -1614,7 +1497,7 @@ object ParticipantAdminCommands { final case class ModifySynchronizerConnection( synchronizerId: Option[PhysicalSynchronizerId], - config: InternalSynchronizerConnectionConfig, + config: SynchronizerConnectionConfig, sequencerConnectionValidation: SequencerConnectionValidation, ) extends Base[v30.ModifySynchronizerRequest, v30.ModifySynchronizerResponse, Unit] { @@ -1691,7 +1574,7 @@ object ParticipantAdminCommands { ), newConfig => v30.PerformManualLsuRequest.SuccessorConnectionConfiguration.Config( - newConfig.toInternal.toProtoV30 + newConfig.toProtoV30 ), ) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerAdminCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerAdminCommands.scala index ce0018bf37..f4cec3ab33 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerAdminCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerAdminCommands.scala @@ -423,10 +423,10 @@ object SequencerAdminCommands { } final case class InitializeFromGenesisStateV2( - topologySnapshot: Seq[ByteString], + topologySnapshot: ByteString, synchronizerParameters: com.digitalasset.canton.protocol.StaticSynchronizerParameters, ) extends GrpcAdminCommand[ - Seq[proto.InitializeSequencerFromGenesisStateV2Request], + proto.InitializeSequencerFromGenesisStateV2Request, proto.InitializeSequencerFromGenesisStateV2Response, InitializeSequencerResponse, ] { @@ -440,21 +440,24 @@ object SequencerAdminCommands { override protected def submitRequest( service: proto.SequencerInitializationServiceGrpc.SequencerInitializationServiceStub, - request: Seq[proto.InitializeSequencerFromGenesisStateV2Request], + request: proto.InitializeSequencerFromGenesisStateV2Request, ): Future[proto.InitializeSequencerFromGenesisStateV2Response] = - GrpcStreamingUtils.streamToServerChunked( + GrpcStreamingUtils.streamToServer( service.initializeSequencerFromGenesisStateV2, - request, + (topologySnapshot: Array[Byte]) => + proto.InitializeSequencerFromGenesisStateV2Request( + topologySnapshot = ByteString.copyFrom(topologySnapshot), + Some(synchronizerParameters.toProtoV30), + ), + new ByteArrayInputStream(request.topologySnapshot.toByteArray), ) override protected def createRequest() - : Either[String, Seq[proto.InitializeSequencerFromGenesisStateV2Request]] = + : Either[String, proto.InitializeSequencerFromGenesisStateV2Request] = Right( - topologySnapshot.map(bytes => - proto.InitializeSequencerFromGenesisStateV2Request( - topologySnapshot = bytes, - Some(synchronizerParameters.toProtoV30), - ) + proto.InitializeSequencerFromGenesisStateV2Request( + topologySnapshot = topologySnapshot, + Some(synchronizerParameters.toProtoV30), ) ) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala index db253637a1..8c5083b5df 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/SequencerBftAdminCommands.scala @@ -3,7 +3,6 @@ package com.digitalasset.canton.admin.api.client.commands -import cats.implicits.toTraverseOps import cats.syntax.either.* import com.digitalasset.canton.sequencer.admin.v30.SequencerBftAdministrationServiceGrpc.SequencerBftAdministrationServiceStub import com.digitalasset.canton.sequencer.admin.v30.{ @@ -15,8 +14,6 @@ import com.digitalasset.canton.sequencer.admin.v30.{ GetPeerNetworkStatusResponse, GetWriteReadinessRequest, GetWriteReadinessResponse, - ListConfiguredEndpointsRequest, - ListConfiguredEndpointsResponse, RemovePeerEndpointRequest, RemovePeerEndpointResponse, SequencerBftAdministrationServiceGrpc, @@ -27,7 +24,6 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.admin.Se OrderingTopology, PeerNetworkStatus, WriteReadiness, - endpointFromProto, endpointIdToProto, endpointToProto, } @@ -96,31 +92,6 @@ object SequencerBftAdminCommands { Either.unit } - final case object ListConfiguredEndpoints - extends BaseSequencerBftAdministrationCommand[ - ListConfiguredEndpointsRequest, - ListConfiguredEndpointsResponse, - Seq[P2PEndpoint], - ] { - - override protected def createRequest(): Either[String, ListConfiguredEndpointsRequest] = Right( - ListConfiguredEndpointsRequest() - ) - - override protected def submitRequest( - service: SequencerBftAdministrationServiceStub, - request: ListConfiguredEndpointsRequest, - ): Future[ListConfiguredEndpointsResponse] = - service.listConfiguredEndpoints(request) - - override protected def handleResponse( - response: ListConfiguredEndpointsResponse - ): Either[String, Seq[P2PEndpoint]] = - response.endpoints - .map(endpointFromProto) - .sequence - } - final case class GetPeerNetworkStatus(endpoints: Option[Iterable[P2PEndpoint.Id]]) extends BaseSequencerBftAdministrationCommand[ GetPeerNetworkStatusRequest, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/TopologyAdminCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/TopologyAdminCommands.scala index 96103fb87f..1eb1951634 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/TopologyAdminCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/TopologyAdminCommands.scala @@ -1295,7 +1295,7 @@ object TopologyAdminCommands { } final case class GetId() - extends BaseInitializationService[v30.GetIdRequest, v30.GetIdResponse, GetIdResult] { + extends BaseInitializationService[v30.GetIdRequest, v30.GetIdResponse, UniqueIdentifier] { override protected def createRequest(): Either[String, v30.GetIdRequest] = Right(v30.GetIdRequest()) @@ -1307,19 +1307,13 @@ object TopologyAdminCommands { override protected def handleResponse( response: v30.GetIdResponse - ): Either[String, GetIdResult] = + ): Either[String, UniqueIdentifier] = if (response.uniqueIdentifier.nonEmpty) - UniqueIdentifier - .fromProtoPrimitive_(response.uniqueIdentifier) - .leftMap(_.message) - .map(id => GetIdResult(response.initialized, Some(id))) + UniqueIdentifier.fromProtoPrimitive_(response.uniqueIdentifier).leftMap(_.message) else - Right(GetIdResult(response.initialized, None)) + Left( + s"Node is not initialized and therefore does not have an Id assigned yet." + ) } - - final case class GetIdResult( - initialized: Boolean, - uniqueIdentifier: Option[UniqueIdentifier], - ) } } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala deleted file mode 100644 index c09e3d9079..0000000000 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.admin.api.client.data - -import cats.syntax.either.* -import com.digitalasset.canton.ProtoDeserializationError -import com.digitalasset.canton.ProtoDeserializationError.UnrecognizedEnum -import com.digitalasset.canton.admin.api.client.data.RegisteredSynchronizer.Status -import com.digitalasset.canton.admin.participant.v30.ListRegisteredSynchronizersResponse.Status as StatusV30 -import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} -import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult -import com.digitalasset.canton.topology.PhysicalSynchronizerId - -/** Information about a registered synchronizer - * @param config - * Configuration to connect to the synchronizer - * @param status - * Status of the connection (e.g., Active, Inactive, LSU) - * @param psid - * If known, the physical synchronizer id of the synchronizer. Always known after the first - * connection. - * @param predecessor - * If set, the LSU predecessor. - * @param isConnected - * Whether the node is currently connected to the synchronizer - */ -final case class RegisteredSynchronizer( - config: SynchronizerConnectionConfig, - status: Status, - psid: ConfiguredPhysicalSynchronizerId, - predecessor: Option[SynchronizerPredecessor], - isConnected: Boolean, -) - -sealed trait ConfiguredPhysicalSynchronizerId extends PrettyPrinting { - def toOption: Option[PhysicalSynchronizerId] - def isDefined: Boolean = toOption.isDefined -} - -object ConfiguredPhysicalSynchronizerId { - def apply(psid: Option[PhysicalSynchronizerId]): ConfiguredPhysicalSynchronizerId = - psid.fold[ConfiguredPhysicalSynchronizerId](UnknownPhysicalSynchronizerId)( - KnownPhysicalSynchronizerId(_) - ) -} - -final case class KnownPhysicalSynchronizerId(psid: PhysicalSynchronizerId) - extends ConfiguredPhysicalSynchronizerId { - override protected def pretty: Pretty[KnownPhysicalSynchronizerId] = - prettyOfString(psid => psid.psid.toLengthLimitedString.unwrap) - - override def toOption: Option[PhysicalSynchronizerId] = Some(psid) -} -case object UnknownPhysicalSynchronizerId extends ConfiguredPhysicalSynchronizerId { - override protected def pretty: Pretty[UnknownPhysicalSynchronizerId.type] = - prettyOfString(_ => "UnknownPhysicalSynchronizerId") - - override def toOption: Option[PhysicalSynchronizerId] = None -} - -object RegisteredSynchronizer { - sealed trait Status extends Serializable with Product with PrettyPrinting - - object Status { - def fromProtoV30(proto: StatusV30): ParsingResult[Status] = - proto match { - case StatusV30.STATUS_UNSPECIFIED => ProtoDeserializationError.FieldNotSet("status").asLeft - case StatusV30.STATUS_ACTIVE => Active.asRight - case StatusV30.STATUS_HARD_MIGRATING_SOURCE => HardMigratingSource.asRight - case StatusV30.STATUS_HARD_MIGRATING_TARGET => HardMigratingTarget.asRight - case StatusV30.STATUS_LSU_SOURCE => LsuSource.asRight - case StatusV30.STATUS_LSU_TARGET => LsuTarget.asRight - case StatusV30.STATUS_INACTIVE => Inactive.asRight - case StatusV30.Unrecognized(unrecognizedValue) => - UnrecognizedEnum("status", unrecognizedValue).asLeft - } - - case object Active extends Status { - override protected def pretty: Pretty[Active.type] = prettyOfString(_ => "Active") - } - - case object HardMigratingSource extends Status { - override protected def pretty: Pretty[HardMigratingSource.type] = - prettyOfString(_ => "HardMigratingSource") - } - case object HardMigratingTarget extends Status { - override protected def pretty: Pretty[HardMigratingTarget.type] = - prettyOfString(_ => "HardMigratingTarget") - } - - case object LsuSource extends Status { - override protected def pretty: Pretty[LsuSource.type] = - prettyOfString(_ => "LSU source") - } - - case object LsuTarget extends Status { - override protected def pretty: Pretty[LsuTarget.type] = - prettyOfString(_ => "LSU target") - } - - case object Inactive extends Status { - override protected def pretty: Pretty[Inactive.type] = prettyOfString(_ => "Inactive") - } - } - -} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala index d71c4c2193..d06c33f095 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala @@ -119,7 +119,9 @@ final case class SequencerConnections( param("sequencer connection pool delays", _.sequencerConnectionPoolDelays), ) - private[canton] def toInternal: SequencerConnectionsInternal = + private[canton] def toInternal(implicit + consoleEnvironment: ConsoleEnvironment + ): SequencerConnectionsInternal = SequencerConnectionsInternal .many( aliasToConnection.toSeq.map { case (_, connection) => connection.toInternal }, @@ -128,15 +130,17 @@ final case class SequencerConnections( submissionRequestAmplification.toInternal, sequencerConnectionPoolDelays.toInternal, ) - .valueOr(err => throw new IllegalArgumentException(s"Invariants not respected: $err")) + .valueOr(consoleEnvironment.raiseError) } object SequencerConnections { - implicit private[data] def sequencerConnectionsToInternalTransformer - : Transformer[SequencerConnections, SequencerConnectionsInternal] = _.toInternal + implicit private[data] def sequencerConnectionsToInternalTransformer(implicit + consoleEnvironment: ConsoleEnvironment + ): Transformer[SequencerConnections, SequencerConnectionsInternal] = _.toInternal - implicit private[data] def sequencerConnectionsFromInternalTransformer - : Transformer[SequencerConnectionsInternal, SequencerConnections] = fromInternal(_) + implicit private[data] def sequencerConnectionsFromInternalTransformer(implicit + consoleEnvironment: ConsoleEnvironment + ): Transformer[SequencerConnectionsInternal, SequencerConnections] = fromInternal(_) def single( connection: SequencerConnection @@ -179,14 +183,14 @@ object SequencerConnections { } yield sequencerConnections } - def many( + def tryMany( connections: Seq[SequencerConnection], sequencerTrustThreshold: PositiveInt, sequencerLivenessMargin: NonNegativeInt, submissionRequestAmplification: SubmissionRequestAmplification, sequencerConnectionPoolDelays: SequencerConnectionPoolDelays, - ): Either[String, SequencerConnections] = - for { + )(implicit consoleEnvironment: ConsoleEnvironment): SequencerConnections = { + val resultE = for { connectionsNE <- NonEmpty.from(connections).toRight("connections should not be empty") result <- many( connectionsNE, @@ -197,24 +201,14 @@ object SequencerConnections { ) } yield result - def tryMany( - connections: Seq[SequencerConnection], - sequencerTrustThreshold: PositiveInt, - sequencerLivenessMargin: NonNegativeInt, - submissionRequestAmplification: SubmissionRequestAmplification, - sequencerConnectionPoolDelays: SequencerConnectionPoolDelays, - )(implicit consoleEnvironment: ConsoleEnvironment): SequencerConnections = - many( - connections, - sequencerTrustThreshold, - sequencerLivenessMargin, - submissionRequestAmplification, - sequencerConnectionPoolDelays, - ).valueOr(consoleEnvironment.raiseError) - - private[canton] def fromInternal(internal: SequencerConnectionsInternal): SequencerConnections = + resultE.valueOr(consoleEnvironment.raiseError) + } + + private[canton] def fromInternal(internal: SequencerConnectionsInternal)(implicit + consoleEnvironment: ConsoleEnvironment + ): SequencerConnections = SequencerConnections - .many( + .tryMany( internal.aliasToConnection.toSeq.map { case (_, connection) => SequencerConnection.fromInternal(connection) }, @@ -223,11 +217,6 @@ object SequencerConnections { SubmissionRequestAmplification.fromInternal(internal.submissionRequestAmplification), SequencerConnectionPoolDelays.fromInternal(internal.sequencerConnectionPoolDelays), ) - .valueOr(err => - throw new IllegalStateException( - s"Invariants of the sequencers connections don't hold: $err" - ) - ) } sealed trait SequencerConnectionValidation { @@ -270,7 +259,7 @@ final case class SubmissionRequestAmplification( paramIfDefined("confirmationResponsePatience", _.confirmationResponsePatienceO), ) - def toInternal: SubmissionRequestAmplificationInternal = + private[canton] def toInternal: SubmissionRequestAmplificationInternal = this.transformInto[SubmissionRequestAmplificationInternal] } @@ -310,7 +299,7 @@ final case class SequencerConnectionPoolDelays( param("subscriptionRequestDelay", _.subscriptionRequestDelay), ) - def toInternal: SequencerConnectionPoolDelaysInternal = + private[canton] def toInternal: SequencerConnectionPoolDelaysInternal = this.transformInto[SequencerConnectionPoolDelaysInternal] } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencingParameters.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencingParameters.scala index d70023c723..9a74ae1f8a 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencingParameters.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencingParameters.scala @@ -11,7 +11,7 @@ import com.google.protobuf.ByteString final case class SequencingParameters(payload: Option[ByteString]) extends PrettyPrinting { - def toInternal: Either[String, protocol.SequencingParameters] = + private[canton] def toInternal: Either[String, protocol.SequencingParameters] = protocol.SequencingParameters .protocolVersionRepresentativeFor(ProtoVersion(30)) .leftMap(_.message) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala index 6bdf09702e..99d20f4ce6 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala @@ -242,7 +242,9 @@ final case class SynchronizerConnectionConfig( ), ) - def toInternal: SynchronizerConnectionConfigInternal = + private[canton] def toInternal(implicit + consoleEnvironment: ConsoleEnvironment + ): SynchronizerConnectionConfigInternal = this.transformInto[SynchronizerConnectionConfigInternal] } @@ -317,8 +319,8 @@ object SynchronizerConnectionConfig { ) } - private[canton] def fromInternal( - internal: SynchronizerConnectionConfigInternal + private[canton] def fromInternal(internal: SynchronizerConnectionConfigInternal)(implicit + consoleEnvironment: ConsoleEnvironment ): SynchronizerConnectionConfig = internal.transformInto[SynchronizerConnectionConfig] } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerParameters.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerParameters.scala index 7cee00e4e8..3b772bb078 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerParameters.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerParameters.scala @@ -288,7 +288,7 @@ final case class DynamicSynchronizerParameters( // https://docs.google.com/document/d/1tpPbzv2s6bjbekVGBn6X5VZuw0oOTHek5c30CBo4UkI/edit#bookmark=id.1dzc6dxxlpca // Originally the validation was done on ledgerTimeRecordTimeTolerance, but was moved to preparationTimeRecordTimeTolerance // instead when the parameter was introduced - def compatibleWithNewPreparationTimeRecordTimeTolerance( + private[canton] def compatibleWithNewPreparationTimeRecordTimeTolerance( newPreparationTimeRecordTimeTolerance: config.NonNegativeFiniteDuration ): Boolean = // If false, a new request may receive the same submission time as a previous request and the previous diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala index 35c0d41cca..3cba8bb352 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala @@ -6,8 +6,6 @@ package com.digitalasset.canton.admin.api.client.data import cats.syntax.traverse.* import com.digitalasset.canton.admin.api.client.data.ListPartiesResult.ParticipantSynchronizers import com.digitalasset.canton.crypto.* -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.admin.v30 @@ -90,30 +88,3 @@ object ListKeyOwnersResult { encryptionKeys <- value.encryptionKeys.traverse(EncryptionPublicKey.fromProtoV30) } yield ListKeyOwnersResult(synchronizerId, owner, signingKeys, encryptionKeys) } - -final case class SynchronizerPredecessor( - psid: PhysicalSynchronizerId, - upgradeTime: CantonTimestamp, - isLateUpgrade: Boolean, -) - -object SynchronizerPredecessor { - def fromProtoV30( - proto: com.digitalasset.canton.admin.topology.v30.SynchronizerPredecessor - ): ParsingResult[SynchronizerPredecessor] = - for { - psid <- PhysicalSynchronizerId.fromProtoPrimitive( - proto.predecessorPhysicalId, - "predecessor_physical_id", - ) - upgradeTime <- ProtoConverter.parseRequired( - CantonTimestamp.fromProtoTimestamp, - "upgrade_time", - proto.upgradeTime, - ) - } yield SynchronizerPredecessor( - psid = psid, - upgradeTime = upgradeTime, - isLateUpgrade = proto.isLateUpgrade, - ) -} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala index 1f9490006b..087e55572d 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala @@ -227,15 +227,12 @@ object ConsoleCommandTimeout { config.NonNegativeDuration.tryFromDuration(30.seconds) val defaultTestingBongTimeout: config.NonNegativeDuration = config.NonNegativeDuration.tryFromDuration(1.minute) - val defaultRequestTimeout: NonNegativeDuration = - config.NonNegativeDuration.tryFromDuration(20.seconds) } /** Timeout settings configuration */ final case class TimeoutSettings( console: ConsoleCommandTimeout = ConsoleCommandTimeout(), processing: ProcessingTimeout = ProcessingTimeout(), - requestTimeout: NonNegativeDuration = NonNegativeDuration.tryFromDuration(40.seconds), ) sealed trait ClockConfig extends Product with Serializable @@ -381,33 +378,44 @@ final case class CantonFeatures( enableRepairCommands: Boolean = false, ) { def featureFlags: Set[FeatureFlag] = - (Seq(FeatureFlag.Stable) ++ (if (enableTestingCommands) Seq(FeatureFlag.Testing) - else Seq()) ++ (if (enablePreviewCommands) Seq(FeatureFlag.Preview) - else Seq()) ++ (if (enableRepairCommands) - Seq( - FeatureFlag.Repair - ) - else Seq())).toSet + (Seq(FeatureFlag.Stable) + ++ (if (enableTestingCommands) Seq(FeatureFlag.Testing) else Seq()) + ++ (if (enablePreviewCommands) Seq(FeatureFlag.Preview) else Seq()) + ++ (if (enableRepairCommands) Seq(FeatureFlag.Repair) else Seq())).toSet } -/** The commonality between [[CantonConfig]] and `SpliceConfig` in the Splice repo, so that either - * Canton or Splice can initialize Canton components that require these configuration elements. +/** Root configuration parameters for a single Canton process. * - * Prefer accepting this type over [[CantonConfig]] at least within app-base, environment setup, - * and integration test framework code. `SharedCantonConfig[?]` is frequently a specific-enough - * type for a function or class argument. + * @param participants + * All locally running participants that this Canton process can connect and operate on. + * @param remoteParticipants + * All remotely running participants to which the console can connect and operate on. + * @param sequencers + * All locally running sequencers that this Canton process can connect and operate on. + * @param remoteSequencers + * All remotely running sequencers that this Canton process can connect and operate on. + * @param mediators + * All locally running mediators that this Canton process can connect and operate on. + * @param remoteMediators + * All remotely running mediators that this Canton process can connect and operate on. + * @param monitoring + * determines how this Canton process can be monitored + * @param parameters + * per-environment parameters to control enabled features and set testing parameters + * @param features + * control which features are enabled */ -trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self] { self: Self => - def portDescription: String - def sequencers: Map[InstanceName, SequencerNodeConfig] - def mediators: Map[InstanceName, MediatorNodeConfig] - def participants: Map[InstanceName, ParticipantNodeConfig] - def remoteSequencers: Map[InstanceName, RemoteSequencerConfig] - def remoteMediators: Map[InstanceName, RemoteMediatorConfig] - def remoteParticipants: Map[InstanceName, RemoteParticipantConfig] - def monitoring: MonitoringConfig - def parameters: CantonParameters - def features: CantonFeatures +final case class CantonConfig( + sequencers: Map[InstanceName, SequencerNodeConfig] = Map.empty, + mediators: Map[InstanceName, MediatorNodeConfig] = Map.empty, + participants: Map[InstanceName, ParticipantNodeConfig] = Map.empty, + remoteSequencers: Map[InstanceName, RemoteSequencerConfig] = Map.empty, + remoteMediators: Map[InstanceName, RemoteMediatorConfig] = Map.empty, + remoteParticipants: Map[InstanceName, RemoteParticipantConfig] = Map.empty, + monitoring: MonitoringConfig = MonitoringConfig(), + parameters: CantonParameters = CantonParameters(), + features: CantonFeatures = CantonFeatures(), +) extends ConfigDefaults[Option[DefaultPorts], CantonConfig] { /** Names of local nodes in order: sequencers, mediators, participants. Order within each group * may be different between runs, but the list may serve as a single reference order in case we @@ -458,6 +466,16 @@ trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self n.unwrap -> c } + /** dump config to string (without sensitive data) */ + /** renders the config as a string (used for dumping config for diagnostic purposes) */ + def dumpString: String = CantonConfig.makeConfidentialString(this) + + /** run a validation on the current config and return possible warning messages */ + private def validate( + ensurePortsSet: Boolean + ): Validated[NonEmpty[Seq[String]], Unit] = + ConfigValidations.validate(this, ensurePortsSet = ensurePortsSet) + private lazy val participantNodeParameters_ : Map[InstanceName, ParticipantNodeParameters] = participants.fmap { participantConfig => val participantParameters = participantConfig.parameters @@ -574,76 +592,6 @@ trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self ), ) - def dumpString: String - - def mergeDynamicChanges(newConfig: Self): Self - - protected def nodePortsDescription( - nodeName: InstanceName, - portDescriptions: Seq[String], - ): String = - s"$nodeName:${portDescriptions.mkString(",")}" - - protected def portDescriptionFromConfig[C]( - config: C - )(apiNamesAndExtractors: Seq[(String, C => ServerConfig)]): Seq[String] = { - def server(name: String, config: ServerConfig): Option[String] = - Option(config).map(c => s"$name=${c.port}") - Option(config) - .map(config => - apiNamesAndExtractors.map { case (name, extractor) => - server(name, extractor(config)) - } - ) - .getOrElse(Seq.empty) - .flatMap(_.toList) - } -} - -/** Root configuration parameters for a single Canton process. - * - * @param participants - * All locally running participants that this Canton process can connect and operate on. - * @param remoteParticipants - * All remotely running participants to which the console can connect and operate on. - * @param sequencers - * All locally running sequencers that this Canton process can connect and operate on. - * @param remoteSequencers - * All remotely running sequencers that this Canton process can connect and operate on. - * @param mediators - * All locally running mediators that this Canton process can connect and operate on. - * @param remoteMediators - * All remotely running mediators that this Canton process can connect and operate on. - * @param monitoring - * determines how this Canton process can be monitored - * @param parameters - * per-environment parameters to control enabled features and set testing parameters - * @param features - * control which features are enabled - */ -final case class CantonConfig( - sequencers: Map[InstanceName, SequencerNodeConfig] = Map.empty, - mediators: Map[InstanceName, MediatorNodeConfig] = Map.empty, - participants: Map[InstanceName, ParticipantNodeConfig] = Map.empty, - remoteSequencers: Map[InstanceName, RemoteSequencerConfig] = Map.empty, - remoteMediators: Map[InstanceName, RemoteMediatorConfig] = Map.empty, - remoteParticipants: Map[InstanceName, RemoteParticipantConfig] = Map.empty, - monitoring: MonitoringConfig = MonitoringConfig(), - parameters: CantonParameters = CantonParameters(), - features: CantonFeatures = CantonFeatures(), -) extends SharedCantonConfig[CantonConfig] { - - /** dump config to string (without sensitive data) */ - /** renders the config as a string (used for dumping config for diagnostic purposes) */ - def dumpString: String = CantonConfig.makeConfidentialString(this) - - /** run a validation on the current config and return possible warning messages */ - private def validate( - ensurePortsSet: Boolean - ): Validated[NonEmpty[Seq[String]], Unit] = { - ConfigValidations.validate(this, ensurePortsSet = ensurePortsSet) - } - /** Produces a message in the structure * "da:admin-api=1,public-api=2;participant1:admin-api=3,ledger-api=4". Helpful for diagnosing * port already bound issues during tests. Allows any config value to be null (can happen with @@ -672,6 +620,27 @@ final case class CantonConfig( .mkString(";") } + protected def nodePortsDescription( + nodeName: InstanceName, + portDescriptions: Seq[String], + ): String = + s"$nodeName:${portDescriptions.mkString(",")}" + + protected def portDescriptionFromConfig[C]( + config: C + )(apiNamesAndExtractors: Seq[(String, C => ServerConfig)]): Seq[String] = { + def server(name: String, config: ServerConfig): Option[String] = + Option(config).map(c => s"$name=${c.port}") + Option(config) + .map(config => + apiNamesAndExtractors.map { case (name, extractor) => + server(name, extractor(config)) + } + ) + .getOrElse(Seq.empty) + .flatMap(_.toList) + } + /** reduces the configuration into a single node configuration (used for external testing) */ def asSingleNode(instanceName: InstanceName): CantonConfig = // based on the assumption that clashing instance names would clash in the console @@ -723,7 +692,7 @@ final case class CantonConfig( private[canton] object CantonNodeParameterConverter { - def general(parent: SharedCantonConfig[?], node: LocalNodeConfig): CantonNodeParameters.General = + def general(parent: CantonConfig, node: LocalNodeConfig): CantonNodeParameters.General = CantonNodeParameters.General.Impl( tracing = parent.monitoring.tracing, delayLoggingThreshold = parent.monitoring.logging.delayLoggingThreshold.toInternal, @@ -744,10 +713,7 @@ private[canton] object CantonNodeParameterConverter { sanitizePublicErrorMessages = parent.monitoring.sanitizePublicErrorMessages, ) - def protocol( - parent: SharedCantonConfig[?], - config: ProtocolConfig, - ): CantonNodeParameters.Protocol = + def protocol(parent: CantonConfig, config: ProtocolConfig): CantonNodeParameters.Protocol = CantonNodeParameters.Protocol.Impl( alphaVersionSupport = parent.parameters.alphaVersionSupport || config.alphaVersionSupport, betaVersionSupport = parent.parameters.betaVersionSupport || config.betaVersionSupport, @@ -2378,7 +2344,7 @@ object CantonConfig { * @return * [[scala.Right]] [[com.typesafe.config.Config]] if parsing was successful. */ - def parseAndMergeConfigs( + private def parseAndMergeConfigs( files: NonEmpty[Seq[File]] )(implicit elc: ErrorLoggingContext): Either[CantonConfigError, Config] = { val baseConfig = ConfigFactory.load() diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala index ed03275c49..f147cb1ef9 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala @@ -5,7 +5,7 @@ package com.digitalasset.canton.console import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.config.{NonNegativeDuration, SharedCantonConfig} +import com.digitalasset.canton.config.{CantonConfig, NonNegativeDuration} import com.digitalasset.canton.console.CommandErrors.ConsoleTimeout import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.environment.{CantonNode, CantonNodeBootstrap} @@ -57,7 +57,7 @@ object AdminCommandRunner { */ trait LedgerApiCommandRunner { - def ledgerApiCommand[Result]( + protected[console] def ledgerApiCommand[Result]( command: GrpcAdminCommand[?, ?, Result] ): ConsoleCommandResult[Result] @@ -103,7 +103,7 @@ trait FeatureFlagFilter extends NamedLogging { protected def consoleEnvironment: ConsoleEnvironment - protected def cantonConfig: SharedCantonConfig[?] = consoleEnvironment.environment.config + protected def cantonConfig: CantonConfig = consoleEnvironment.environment.config protected def check[T](flag: FeatureFlag)(command: => T): T = FeatureFlagFilter.checkEnabled(noTracingLogger)( diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala index 7c6c0ee4d5..9389976626 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.console.commands.GlobalSecretKeyAdministration import com.digitalasset.canton.crypto.{Crypto, Fingerprint} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.environment.{CantonEnvironment, Environment} +import com.digitalasset.canton.environment.Environment import com.digitalasset.canton.lifecycle.{FlagCloseable, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil @@ -62,12 +62,15 @@ object NodeReferences { NodeReferences[ParticipantReference, RemoteParticipantReference, LocalParticipantReference] } -trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing { - type Config <: SharedCantonConfig[Config] - - val environment: Environment[Config] - - val consoleOutput: ConsoleOutput +/** The environment in which console commands are evaluated. + */ +@SuppressWarnings(Array("org.wartremover.warts.Any")) // required for `Binding[_]` usage +class ConsoleEnvironment( + val environment: Environment, + val consoleOutput: ConsoleOutput = StandardConsoleOutput, +) extends NamedLogging + with FlagCloseable + with NoTracing { override protected val loggerFactory: NamedLoggerFactory = environment.loggerFactory @@ -101,7 +104,7 @@ trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing private[console] def predefCode(interactive: Boolean, noTty: Boolean = false): String = ConsoleEnvironmentBinding.predefCode(interactive, noTty) - val tracer: Tracer = environment.tracerProvider.tracer + private[console] val tracer: Tracer = environment.tracerProvider.tracer /** Definition of the startup order of local instances. Nodes support starting up in any order * however to avoid delays/warnings we opt to start in the most desirable order for simple @@ -109,7 +112,7 @@ trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing * return a int for the instance (typically just a static value based on type), and then the * console will start these instances for lower to higher values. */ - def startupOrderPrecedence(instance: LocalInstanceReference): Int = + private def startupOrderPrecedence(instance: LocalInstanceReference): Int = instance match { case _: LocalSequencerReference => 1 // everything depends on a sequencer so start that first @@ -321,7 +324,7 @@ trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing Help.Summary("Help with console commands"), Help.Description("Type help(\"\") for detailed help for ."), Help.Topic(Help.defaultTopLevelTopic), - ) + ), ) :+ (Help.Item( "exit", @@ -519,20 +522,6 @@ trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing def startAll(): Unit = runE(environment.startAll()) def stopAll(): Unit = runE(environment.stopAll()) -} - -/** The environment in which console commands are evaluated. - */ -@SuppressWarnings(Array("org.wartremover.warts.Any")) // required for `Binding[_]` usage -class CantonConsoleEnvironment( - override val environment: CantonEnvironment, - val consoleOutput: ConsoleOutput = StandardConsoleOutput, -) extends ConsoleEnvironment - with NamedLogging - with FlagCloseable - with NoTracing { - - override type Config = CantonConfig } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala index 2bd01ef147..0f8a608186 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala @@ -19,7 +19,6 @@ import com.digitalasset.canton.config.{ ClientConfig, ConsoleCommandTimeout, NonNegativeDuration, - SharedCantonConfig, } import com.digitalasset.canton.environment.Environment import com.digitalasset.canton.lifecycle.OnShutdownRunner @@ -27,14 +26,12 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.{ CantonGrpcUtil, ClientChannelBuilder, - GrpcError, GrpcManagedChannel, } import com.digitalasset.canton.tracing.{Spanning, TraceContext} import com.digitalasset.canton.util.Mutex import io.opentelemetry.api.trace.Tracer -import java.util.concurrent.atomic.AtomicReference import scala.collection.concurrent.TrieMap import scala.concurrent.duration.{Duration, FiniteDuration} import scala.concurrent.{ExecutionContextExecutor, Future} @@ -56,14 +53,6 @@ class GrpcAdminCommandRunner( with OnShutdownRunner with Spanning { - private val retryPolicyV = - new AtomicReference[GrpcAdminCommand[?, ?, ?] => GrpcError => Boolean](_ => _ => false) - - def retryPolicy: GrpcAdminCommand[?, ?, ?] => GrpcError => Boolean = retryPolicyV.get() - - def setRetryPolicy(policy: GrpcAdminCommand[?, ?, ?] => GrpcError => Boolean): Unit = - retryPolicyV.set(policy) - private val grpcRunner = new GrpcCtlRunner( apiLoggingConfig.maxMessageLines, apiLoggingConfig.maxStringLength, @@ -124,7 +113,6 @@ class GrpcAdminCommandRunner( channel, token, callTimeout.duration, - retryPolicy(command), ) } yield result ( @@ -190,7 +178,7 @@ class GrpcAdminCommandRunner( object GrpcAdminCommandRunner { def apply( - environment: Environment[? <: SharedCantonConfig[?]], + environment: Environment, apiName: String, ): GrpcAdminCommandRunner = new GrpcAdminCommandRunner( diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala index 3c7a6bcfba..e6f3a3573b 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala @@ -16,7 +16,7 @@ import com.digitalasset.canton.admin.api.client.data.{ DynamicSynchronizerParameters as ConsoleDynamicSynchronizerParameters, NodeStatus, } -import com.digitalasset.canton.config.{LocalNodeConfig, SharedCantonConfig} +import com.digitalasset.canton.config.LocalNodeConfig import com.digitalasset.canton.console.CommandErrors.CommandError import com.digitalasset.canton.console.HealthDumpGenerator.ParametersWithValidity import com.digitalasset.canton.environment.Environment @@ -38,7 +38,7 @@ import java.time.Instant * the core of the implementation of the HealthDump gRPC endpoint. */ class HealthDumpGenerator( - val environment: Environment[? <: SharedCantonConfig[?]], + val environment: Environment, val grpcAdminCommandRunner: GrpcAdminCommandRunner, override val loggerFactory: NamedLoggerFactory, ) extends NamedLogging { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala index a1e338dc90..13b8842e1a 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/InstanceReference.scala @@ -93,7 +93,7 @@ trait InstanceReference @inline final override def uid: UniqueIdentifier = id.uid val name: String - protected val instanceType: String + protected[canton] val instanceType: String protected[canton] def executionContext: ExecutionContext @@ -152,7 +152,7 @@ trait LocalInstanceReference extends InstanceReference with NoTracing { val name: String val consoleEnvironment: ConsoleEnvironment - protected[console] val nodes: Nodes[CantonNode, CantonNodeBootstrap[CantonNode]] + private[console] val nodes: Nodes[CantonNode, CantonNodeBootstrap[CantonNode]] @Help.Summary("Database related operations") @Help.Group("Database") @@ -216,7 +216,7 @@ trait LocalInstanceReference extends InstanceReference with NoTracing { private def filterByNodeAndAttribute( attributes: Map[String, String] )(value: MetricValue): Boolean = - value.attributes.get("node_name").contains(name) && attributes.forall { case (k, v) => + value.attributes.get("node").contains(name) && attributes.forall { case (k, v) => value.attributes.get(k).contains(v) } @@ -390,7 +390,7 @@ trait LocalInstanceReference extends InstanceReference with NoTracing { else NodeNotStarted.ErrorCanton(this) - override def adminCommand[Result]( + override protected[console] def adminCommand[Result]( grpcCommand: GrpcAdminCommand[?, ?, Result] ): ConsoleCommandResult[Result] = runCommandIfRunning( @@ -408,7 +408,7 @@ trait RemoteInstanceReference extends InstanceReference { def config: NodeConfig - override def adminCommand[Result]( + override protected[console] def adminCommand[Result]( grpcCommand: GrpcAdminCommand[?, ?, Result] ): ConsoleCommandResult[Result] = consoleEnvironment.grpcAdminCommandRunner.runCommand( @@ -450,13 +450,13 @@ class ExternalLedgerApiClient( override val loggerFactory: NamedLoggerFactory = consoleEnvironment.environment.loggerFactory.append("client", name) - override def ledgerApiCommand[Result]( + override protected[console] def ledgerApiCommand[Result]( command: GrpcAdminCommand[?, ?, Result] ): ConsoleCommandResult[Result] = consoleEnvironment.grpcLedgerCommandRunner .runCommand("sourceLedger", command, FullClientConfig(hostname, port, tls), token) - override def optionallyAwait[Tx]( + override private[canton] def optionallyAwait[Tx]( tx: Tx, txId: String, txSynchronizerId: String, @@ -718,7 +718,7 @@ class RemoteParticipantReference(environment: ConsoleEnvironment, override val n def config: RemoteParticipantConfig = consoleEnvironment.environment.config.remoteParticipantsByString(name) - override def ledgerApiCommand[Result]( + override protected[console] def ledgerApiCommand[Result]( command: GrpcAdminCommand[?, ?, Result] ): ConsoleCommandResult[Result] = consoleEnvironment.grpcLedgerCommandRunner.runCommand( @@ -776,7 +776,7 @@ class LocalParticipantReference( case ClockConfig.RemoteClock(_) => None } - override protected[console] val nodes = consoleEnvironment.environment.participants + override private[console] val nodes = consoleEnvironment.environment.participants @Help.Summary("Return participant config") def config: ParticipantNodeConfig = @@ -803,7 +803,7 @@ class LocalParticipantReference( @Help.Group("Commitments") override def commitments: LocalCommitmentsAdministrationGroup = commitments_ - override def ledgerApiCommand[Result]( + override protected[console] def ledgerApiCommand[Result]( command: GrpcAdminCommand[?, ?, Result] ): ConsoleCommandResult[Result] = runCommandIfRunning( @@ -1351,12 +1351,6 @@ abstract class SequencerReference( ) } - @Help.Summary("List peer endpoints configured and stored in DB") - def list_configured_peer_endpoints(): Seq[P2PEndpoint] = - consoleEnvironment.run { - runner.adminCommand(SequencerBftAdminCommands.ListConfiguredEndpoints) - } - @Help.Summary("Get peer network status") def get_peer_network_status( endpoints: Option[Iterable[BftBlockOrdererConfig.EndpointId]] = None @@ -1451,7 +1445,7 @@ class LocalSequencerReference( .asSequencerConnection(sequencerAlias = SequencerAlias.tryCreate(name), sequencerId = None) ) - val nodes: SequencerNodes = + private[console] val nodes: SequencerNodes = consoleEnvironment.environment.sequencers override protected[console] def runningNode: Option[SequencerNodeBootstrap] = @@ -1592,7 +1586,7 @@ class LocalMediatorReference(consoleEnvironment: ConsoleEnvironment, val name: S override def config: MediatorNodeConfig = consoleEnvironment.environment.config.mediatorsByString(name) - val nodes: MediatorNodes = consoleEnvironment.environment.mediators + private[console] val nodes: MediatorNodes = consoleEnvironment.environment.mediators override protected[console] def runningNode: Option[MediatorNodeBootstrap] = nodes.getRunning(name) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ParticipantReferencesExtensions.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ParticipantReferencesExtensions.scala index 847ec0aadb..18a86a105e 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ParticipantReferencesExtensions.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ParticipantReferencesExtensions.scala @@ -13,7 +13,6 @@ import com.digitalasset.canton.console.commands.ParticipantCommands import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger} import com.digitalasset.canton.topology.{PhysicalSynchronizerId, SynchronizerId} -import com.google.protobuf.ByteString class ParticipantReferencesExtensions(participants: Seq[ParticipantReference])(implicit override val consoleEnvironment: ConsoleEnvironment @@ -43,7 +42,6 @@ class ParticipantReferencesExtensions(participants: Seq[ParticipantReference])(i synchronizeVetting: Boolean = true, expectedMainPackageId: String = "", requestHeaders: Map[String, String] = Map(), - darDataO: Option[ByteString] = None, ): Map[ParticipantReference, String] = { val res = ConsoleCommandResult.runAll(participants)( ParticipantCommands.dars @@ -57,7 +55,6 @@ class ParticipantReferencesExtensions(participants: Seq[ParticipantReference])(i expectedMainPackageId = expectedMainPackageId, requestHeaders = requestHeaders, logger, - darDataO, ) ) if (synchronizeVetting && vetAllPackages) { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala index 390b3c85bd..7ad0e1635b 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala @@ -9,10 +9,7 @@ import cats.syntax.either.* import cats.syntax.parallel.* import com.daml.nonempty.NonEmpty import com.daml.nonempty.catsinstances.* -import com.digitalasset.canton.admin.api.client.data.{ - KnownPhysicalSynchronizerId, - ListPartiesResult, -} +import com.digitalasset.canton.admin.api.client.data.ListPartiesResult import com.digitalasset.canton.config.ConsoleCommandTimeout import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.{ @@ -39,6 +36,7 @@ import com.digitalasset.canton.topology.transaction.{ } import com.digitalasset.canton.topology.{ ExternalParty, + KnownPhysicalSynchronizerId, Namespace, ParticipantId, PartyId, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala index 73fa7e5623..c7aa9cdac5 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala @@ -132,7 +132,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper implicit protected[canton] lazy val executionContext: ExecutionContext = consoleEnvironment.environment.executionContext - implicit val consoleEnvironment: ConsoleEnvironment + implicit protected val consoleEnvironment: ConsoleEnvironment protected val name: String @@ -163,14 +163,14 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper ) ) - def optionallyAwait[Tx]( + private[canton] def optionallyAwait[Tx]( tx: Tx, txId: String, txSynchronizerId: String, optTimeout: Option[config.NonNegativeDuration], ): Tx - def timeouts: ConsoleCommandTimeout = consoleEnvironment.commandTimeouts + private def timeouts: ConsoleCommandTimeout = consoleEnvironment.commandTimeouts protected def defaultLimit: PositiveInt = consoleEnvironment.environment.config.parameters.console.defaultLimit @@ -3611,7 +3611,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper trait LedgerApiAdministration extends BaseLedgerApiAdministration { this: LedgerApiCommandRunner & AdminCommandRunner & NamedLogging & FeatureFlagFilter => - implicit val consoleEnvironment: ConsoleEnvironment + implicit protected val consoleEnvironment: ConsoleEnvironment protected val name: String import com.digitalasset.canton.util.ShowUtil.* @@ -3746,7 +3746,7 @@ trait LedgerApiAdministration extends BaseLedgerApiAdministration { } } - override def optionallyAwait[T]( + override private[canton] def optionallyAwait[T]( update: T, updateId: String, txSynchronizerId: String, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala index 96c06bbf63..b4ebf316df 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala @@ -75,6 +75,7 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.time.NonNegativeFiniteDuration import com.digitalasset.canton.topology.transaction.GrpcConnection import com.digitalasset.canton.topology.{ + ConfiguredPhysicalSynchronizerId, ParticipantId, PartyId, PhysicalSynchronizerId, @@ -85,7 +86,6 @@ import com.digitalasset.canton.topology.{ import com.digitalasset.canton.tracing.NoTracing import com.digitalasset.canton.util.* import com.digitalasset.canton.{SequencerAlias, SynchronizerAlias, config} -import com.google.protobuf.ByteString import io.grpc.Context import java.time.Instant @@ -113,7 +113,6 @@ private[console] object ParticipantCommands { expectedMainPackageId: String, requestHeaders: Map[String, String], logger: TracedLogger, - darDataO: Option[ByteString] = None, ): ConsoleCommandResult[String] = runner .adminCommand( @@ -127,7 +126,6 @@ private[console] object ParticipantCommands { expectedMainPackageId, requestHeaders, logger, - darDataO, ) ) .flatMap { @@ -250,7 +248,7 @@ private[console] object ParticipantCommands { config: SynchronizerConnectionConfig, performHandshake: Boolean, validation: SequencerConnectionValidation, - ): ConsoleCommandResult[Unit] = + )(implicit consoleEnvironment: ConsoleEnvironment): ConsoleCommandResult[Unit] = runner.adminCommand( ParticipantAdminCommands.SynchronizerConnectivity .RegisterSynchronizer( @@ -264,7 +262,7 @@ private[console] object ParticipantCommands { runner: AdminCommandRunner, config: SynchronizerConnectionConfig, validation: SequencerConnectionValidation, - ): ConsoleCommandResult[Unit] = + )(implicit consoleEnvironment: ConsoleEnvironment): ConsoleCommandResult[Unit] = runner.adminCommand( ParticipantAdminCommands.SynchronizerConnectivity .ConnectSynchronizer(config.toInternal, validation.toInternal) @@ -400,14 +398,13 @@ class ParticipantTestingGroup( def fetch_synchronizer_time( synchronizer: Synchronizer, timeout: config.NonNegativeDuration = consoleEnvironment.commandTimeouts.ledgerCommand, - freshnessBound: config.NonNegativeFiniteDuration = config.NonNegativeFiniteDuration.Zero, ): CantonTimestamp = check(FeatureFlag.Testing) { consoleEnvironment.run { adminCommand( SynchronizerTimeCommands.FetchTime( synchronizer.some, - freshnessBound.toInternal, + NonNegativeFiniteDuration.Zero, timeout, ) ) @@ -1720,7 +1717,6 @@ trait ParticipantAdministration extends FeatureFlagFilter { synchronizeVetting: Boolean = true, expectedMainPackageId: String = "", requestHeaders: Map[String, String] = Map(), - darDataO: Option[ByteString] = None, ): String = { val res = consoleEnvironment.runE { for { @@ -2570,20 +2566,14 @@ trait ParticipantAdministration extends FeatureFlagFilter { |currently connected to the synchronizer.""" ) def list_registered() - : Seq[(SynchronizerConnectionConfig, ConfiguredPhysicalSynchronizerId, Boolean)] = - consoleEnvironment.run { - adminCommand( - ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers - ) + : Seq[(SynchronizerConnectionConfig, ConfiguredPhysicalSynchronizerId, Boolean)] = { + val result = consoleEnvironment.run { + adminCommand(ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers) } - - @Help.Summary("List all the configured synchronizers of this participant") - def list_all_registered(): Seq[RegisteredSynchronizer] = - consoleEnvironment.run { - adminCommand( - ParticipantAdminCommands.SynchronizerConnectivity.ListAllRegisteredSynchronizers - ) + result.map { case (internalConfig, psid, connected) => + (SynchronizerConnectionConfig.fromInternal(internalConfig), psid, connected) } + } @Help.Summary("Returns true if a synchronizer is registered using the given alias") def is_registered(synchronizerAlias: SynchronizerAlias): Boolean = @@ -2634,7 +2624,7 @@ trait ParticipantAdministration extends FeatureFlagFilter { consoleEnvironment.runE { for { registeredSynchronizers <- adminCommand( - ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers + ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers ).toEither cfg <- registeredSynchronizers .collectFirst { @@ -2642,7 +2632,7 @@ trait ParticipantAdministration extends FeatureFlagFilter { if config.synchronizerAlias == synchronizerAlias && physicalSynchronizerId .forall(requestedPsid => knownPsid.toOption.contains(requestedPsid)) => - config + SynchronizerConnectionConfig.fromInternal(config) } .toRight( s"No active synchronizer $synchronizerAlias configured" + physicalSynchronizerId @@ -2744,7 +2734,7 @@ trait ParticipantAdministration extends FeatureFlagFilter { ): Unit = consoleEnvironment.run { adminCommand( ParticipantAdminCommands.SynchronizerConnectivity - .PerformManualLsu(currentPsid, successorPsid, upgradeTime, Right(config)) + .PerformManualLsu(currentPsid, successorPsid, upgradeTime, Right(config.toInternal)) ) } } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala index 5267b2f78c..2b3c26d649 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantRepairAdministration.scala @@ -4,6 +4,7 @@ package com.digitalasset.canton.console.commands import better.files.File +import cats.syntax.either.* import cats.syntax.foldable.* import com.digitalasset.canton.admin.api.client.commands.ParticipantAdminCommands import com.digitalasset.canton.admin.api.client.data.{ @@ -25,6 +26,7 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.grpc.OutputFileStreamObserver import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.participant.admin.data.{ + ActiveContract, ContractImportMode, RepairContract, RepresentativePackageIdOverride, @@ -32,11 +34,12 @@ import com.digitalasset.canton.participant.admin.data.{ import com.digitalasset.canton.protocol.LfContractId import com.digitalasset.canton.topology.{PartyId, PhysicalSynchronizerId, SynchronizerId} import com.digitalasset.canton.tracing.NoTracing +import com.digitalasset.canton.util.ResourceUtil import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.canton.{ReassignmentCounter, SequencerCounter, SynchronizerAlias} -import java.util.UUID import io.grpc.Context +import java.util.UUID import scala.util.{Failure, Success, Try} class ParticipantRepairAdministration( @@ -367,7 +370,44 @@ class ParticipantRepairAdministration( representativePackageIdOverride: RepresentativePackageIdOverride = RepresentativePackageIdOverride.NoOverride, excludedStakeholders: Set[PartyId] = Set.empty, - ): Unit = ??? + ): Unit = { + + val temporaryFile = File.newTemporaryFile(suffix = ".gz") + val outputStream = temporaryFile.newGzipOutputStream() + val resolvedWorkflowIdPrefix = + if (workflowIdPrefix.isEmpty) s"import-${UUID.randomUUID}" else workflowIdPrefix + + ResourceUtil.withResource(outputStream) { outputStream => + contracts + .traverse_ { repairContract => + for { + lapiContract <- RepairContract.toLapiActiveContract(repairContract, synchronizerId) + activeContract = ActiveContract.create(lapiContract)(protocolVersion) + _ = activeContract.writeDelimitedTo(outputStream) + } yield outputStream.flush() + } + .valueOr(err => + consoleEnvironment.raiseError(s"Unable to add contract data to stream: $err") + ) + } + + check(FeatureFlag.Repair) { + consoleEnvironment.run { + runner.adminCommand( + ParticipantAdminCommands.ParticipantRepairManagement.ImportAcs( + temporaryFile.toJava, + resolvedWorkflowIdPrefix, + contractImportMode, + representativePackageIdOverride, + excludedStakeholders, + synchronizerId, + ) + ) + } + } + + temporaryFile.delete(swallowIOExceptions = true) + } @Help.Summary("Purge the data of a deactivated synchronizer") @Help.Description( diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/PartiesAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/PartiesAdministration.scala index 1db0f07e93..f63bfcc113 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/PartiesAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/PartiesAdministration.scala @@ -50,7 +50,6 @@ import com.digitalasset.canton.{LedgerParticipantId, SynchronizerAlias, config} import com.google.common.annotations.VisibleForTesting import io.grpc.Context -import java.io.FileInputStream import java.time.Instant import java.util.UUID @@ -883,7 +882,7 @@ class ParticipantPartiesAdministrationGroup( consoleEnvironment.run { reference.adminCommand( ParticipantAdminCommands.PartyManagement.ImportPartyAcs( - new FileInputStream(new java.io.File(importFilePath)), + new java.io.File(importFilePath), synchronizerId, if (workflowIdPrefix.nonEmpty) workflowIdPrefix else s"import-${UUID.randomUUID}", contractImportMode, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala index 9e2ea2b493..ad86bf05b3 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/SequencerAdministration.scala @@ -201,7 +201,7 @@ class SequencerAdministration(node: SequencerReference) extends ConsoleCommandGr consoleEnvironment.run { runner.adminCommand( InitializeFromGenesisStateV2( - Seq(genesisState), + genesisState, synchronizerParameters.toInternal, ) ) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministrationGroup.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministrationGroup.scala index 1008861e46..e4a1118b27 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministrationGroup.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/TopologyAdministrationGroup.scala @@ -9,21 +9,22 @@ import cats.syntax.traverse.* import com.daml.nameof.NameOf.functionFullName import com.daml.nonempty.NonEmpty import com.digitalasset.base.error.RpcError -import com.digitalasset.canton.admin.api.client.commands.TopologyAdminCommands.Init.GetIdResult import com.digitalasset.canton.admin.api.client.commands.TopologyAdminCommands.Write.GenerateTransactions import com.digitalasset.canton.admin.api.client.commands.{GrpcAdminCommand, TopologyAdminCommands} import com.digitalasset.canton.admin.api.client.data.topology.* import com.digitalasset.canton.admin.api.client.data.{ + DynamicSynchronizerParameters as ConsoleDynamicSynchronizerParameters, SequencingParameters, TopologyQueueStatus, - DynamicSynchronizerParameters as ConsoleDynamicSynchronizerParameters, } -import com.digitalasset.canton.{config, networking} import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.config.{ConsoleCommandTimeout, NonNegativeDuration} +import com.digitalasset.canton.console.CommandErrors.{CommandError, GenericCommandError} +import com.digitalasset.canton.console.ConsoleEnvironment.Implicits.* import com.digitalasset.canton.console.{ AdminCommandRunner, CommandErrors, + CommandSuccessful, ConsoleCommandResult, ConsoleEnvironment, ConsoleMacros, @@ -33,7 +34,6 @@ import com.digitalasset.canton.console.{ Helpful, InstanceReference, } -import com.digitalasset.canton.console.CommandErrors.GenericCommandError import com.digitalasset.canton.crypto.* import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps @@ -42,7 +42,6 @@ import com.digitalasset.canton.grpc.{ByteStringStreamObserver, OutputFileStreamO import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId.Authorized -import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId.* import com.digitalasset.canton.topology.admin.grpc.{BaseQuery, TopologyStoreId} import com.digitalasset.canton.topology.admin.v30.{ ExportTopologySnapshotResponse, @@ -65,9 +64,10 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.BinaryFileUtil import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.version.{ProtocolVersion, ProtocolVersionValidation} +import com.digitalasset.canton.{config, networking} import com.digitalasset.daml.lf.data.Ref.PackageId import com.google.protobuf.ByteString -import io.grpc.{Context, Status} +import io.grpc.Context import java.net.URI import java.time.Duration @@ -190,7 +190,7 @@ class TopologyAdministrationGroup( waitForReady, ) - private def getIdCommand(): ConsoleCommandResult[GetIdResult] = + private def getIdCommand(): ConsoleCommandResult[UniqueIdentifier] = adminCommand(TopologyAdminCommands.Init.GetId()) // small cache to avoid repetitive calls to fetchId (as the id is immutable once set) @@ -203,13 +203,15 @@ class TopologyAdministrationGroup( private[console] def idHelper[T]( apply: UniqueIdentifier => T ): T = - maybeIdHelper(apply).getOrElse( - throw Status.UNAVAILABLE - .withDescription( - s"Node does not have an Id assigned yet." - ) - .asRuntimeException() - ) + apply(idCache.get() match { + case Some(v) => v + case None => + val r = consoleEnvironment.run { + getIdCommand() + } + idCache.set(Some(r)) + r + }) private[console] def maybeIdHelper[T]( apply: UniqueIdentifier => T @@ -217,11 +219,14 @@ class TopologyAdministrationGroup( (idCache.get() match { case Some(v) => Some(v) case None => - val r = consoleEnvironment.run { - getIdCommand() + consoleEnvironment.run { + CommandSuccessful(getIdCommand() match { + case CommandSuccessful(v) => + idCache.set(Some(v)) + Some(v) + case _: CommandError => None + }) } - r.uniqueIdentifier.foreach(id => idCache.set(Some(id))) - r.uniqueIdentifier }).map(apply) @Help.Summary("Topology synchronisation helpers", FeatureFlag.Preview) @@ -2709,9 +2714,9 @@ class TopologyAdministrationGroup( ( serial.increment, // first filter out all existing packages that either get re-added (i.e. modified) or removed - item.packages.filter(vp => - !allChangedPackageIds.contains(vp.packageId) - ) /* now we can add all the adds the also haven't been in the remove set */ ++ adds, + item.packages.filter(vp => !allChangedPackageIds.contains(vp.packageId)) + // now we can add all the adds the also haven't been in the remove set + ++ adds, ) case Some( ListVettedPackagesResult( @@ -2864,7 +2869,7 @@ class TopologyAdministrationGroup( adminCommand( TopologyAdminCommands.Read.ListMediatorSynchronizerState( BaseQuery( - synchronizerId.map(TopologyStoreId.Synchronizer(_)), + synchronizerId, proposals, timeQuery, operation, @@ -3056,7 +3061,7 @@ class TopologyAdministrationGroup( mustFullyAuthorize: Boolean = false, ): SignedTopologyTransaction[TopologyChangeOp, MediatorSynchronizerState] = { - val mediatorStateResult = list(synchronizerId = Some(synchronizerId), group = Some(group)) + val mediatorStateResult = list(synchronizerId = synchronizerId, group = Some(group)) .maxByOption(_.context.serial) .getOrElse(throw new IllegalArgumentException(s"Unknown mediator group $group")) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala index fc7fffe002..e05ddfd4b2 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala @@ -100,7 +100,6 @@ object DeclarativeApiManager { traceContext: TraceContext, ): EitherT[Future, String, DeclarativeApiHandle[ParticipantNodeConfig]] = { val myLoggerFactory = loggerFactory.append("participant", name) - val api = new DeclarativeParticipantApi( name, initialConfig.ledgerApi.clientConfig, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala index e88d2946b1..2f1268f1dc 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala @@ -123,8 +123,7 @@ class DeclarativeParticipantApi( ): Either[String, ParticipantId] = for { _ <- createDarDirectoryIfNecessary(config.fetchedDarDirectory, config.dars) - result <- queryAdminApi(TopologyAdminCommands.Init.GetId()) - uid <- result.uniqueIdentifier.toRight("Node is not initialized") + uid <- queryAdminApi(TopologyAdminCommands.Init.GetId()) } yield ParticipantId(uid) override protected def sync(config: DeclarativeParticipantConfig, context: ParticipantId)(implicit @@ -712,12 +711,9 @@ class DeclarativeParticipantApi( ) def fetchConnections(): Either[String, Seq[(SynchronizerAlias, DeclarativeConnectionConfig)]] = - queryAdminApi( - ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers - ) - .map(_.map { case (synchronizerConnectionConfig, _, _) => - toDeclarative(synchronizerConnectionConfig.toInternal) - }) + queryAdminApi(ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers) + .map(_.map { case (synchronizerConnectionConfig, _, _) => synchronizerConnectionConfig } + .map(toDeclarative)) def getConnection( alias: SynchronizerAlias @@ -730,7 +726,7 @@ class DeclarativeParticipantApi( // cannot really remove connections for now, just disconnect and disable for { currentO <- queryAdminApi( - ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers + ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers ).map(_.collectFirst { case (config, psidO, _) if config.synchronizerAlias == synchronizerAlias => (config, psidO) @@ -745,7 +741,7 @@ class DeclarativeParticipantApi( (currentConfig, psidO) = current _ <- queryAdminApi( ParticipantAdminCommands.SynchronizerConnectivity.ModifySynchronizerConnection( - config = currentConfig.copy(manualConnect = true).toInternal, + config = currentConfig.copy(manualConnect = true), synchronizerId = psidO.toOption, sequencerConnectionValidation = SequencerConnectionValidation.Disabled, ) @@ -931,28 +927,25 @@ class DeclarativeParticipantApi( )(implicit traceContext: TraceContext ): Either[String, UpdateResult] = - queryAdminApi(TopologyAdminCommands.Init.GetId()).flatMap { participantIdResult => + queryAdminApi(TopologyAdminCommands.Init.GetId()).flatMap { participantId => queryAdminApi(ListConnectedSynchronizers()).flatMap { connectedSynchronizers => val want = computeWanted(mirrorDarsIfNecessary(fetchDarDirectory, dars), connectedSynchronizers) def fetchVettedPackages(store: Option[TopologyStoreId]) = - participantIdResult.uniqueIdentifier.toRight("Node is not initialized").flatMap { - participantId => - queryAdminApi( - TopologyAdminCommands.Read.ListVettedPackages( - BaseQuery( - store = store, - proposals = false, - timeQuery = TimeQuery.HeadState, - ops = Some(TopologyChangeOp.Replace), - filterSigningKey = "", - protocolVersion = None, - ), - filterParticipant = ParticipantId(participantId).filterString, - ) - ) - } + queryAdminApi( + TopologyAdminCommands.Read.ListVettedPackages( + BaseQuery( + store = store, + proposals = false, + timeQuery = TimeQuery.HeadState, + ops = Some(TopologyChangeOp.Replace), + filterSigningKey = "", + protocolVersion = None, + ), + filterParticipant = ParticipantId(participantId).filterString, + ) + ) def fetchDars(limit: PositiveInt): Either[String, Seq[((String, SynchronizerId), String)]] = for { @@ -1023,7 +1016,6 @@ class DeclarativeParticipantApi( expectedMainPackageId = "", requestHeaders = Map.empty, logger, - None, ) ).map(_ => ()) }, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala index 4151149939..68c622d952 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala @@ -9,14 +9,14 @@ import com.digitalasset.canton.participant.ParticipantNodeBootstrapFactoryImpl import com.digitalasset.canton.synchronizer.mediator.MediatorNodeBootstrapFactoryImpl import com.digitalasset.canton.synchronizer.sequencer.SequencerNodeBootstrapFactoryImpl -object CommunityEnvironmentFactory extends CantonEnvironmentFactory { +object CommunityEnvironmentFactory extends EnvironmentFactory { override def create( config: CantonConfig, loggerFactory: NamedLoggerFactory, testingConfigInternal: TestingConfigInternal, - ): CantonEnvironment = - new CantonEnvironment( + ): Environment = + new Environment( config, testingConfigInternal, ParticipantNodeBootstrapFactoryImpl, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala index 182d0c25b9..a91fe05940 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala @@ -8,11 +8,10 @@ import cats.data.EitherT import cats.syntax.either.* import com.daml.grpc.adapter.ExecutionSequencerFactory import com.daml.metrics.ExecutorServiceMetrics -import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext, MetricsInfoFilter} +import com.daml.metrics.api.{HistogramInventory, MetricsContext, MetricsInfoFilter} import com.digitalasset.canton.concurrent.* import com.digitalasset.canton.config.* import com.digitalasset.canton.console.{ - CantonConsoleEnvironment, ConsoleEnvironment, ConsoleOutput, GrpcAdminCommandRunner, @@ -26,7 +25,7 @@ import com.digitalasset.canton.environment.Environment.* import com.digitalasset.canton.lifecycle.LifeCycle import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.metrics.MetricsConfig.JvmMetrics -import com.digitalasset.canton.metrics.{CantonHistograms, DbStorageHistograms, MetricsRegistry} +import com.digitalasset.canton.metrics.{CantonHistograms, MetricsRegistry} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil import com.digitalasset.canton.participant.* import com.digitalasset.canton.participant.config.ParticipantNodeConfig @@ -61,8 +60,8 @@ import scala.util.control.NonFatal /** Holds all significant resources held by this process. */ -abstract class Environment[Config <: SharedCantonConfig[Config]]( - initialConfig: Config, +class Environment( + initialConfig: CantonConfig, val testingConfig: TestingConfigInternal, participantNodeFactory: ParticipantNodeBootstrapFactory, sequencerNodeFactory: SequencerNodeBootstrapFactory, @@ -72,21 +71,15 @@ abstract class Environment[Config <: SharedCantonConfig[Config]]( with AutoCloseable with NoTracing { - type Console <: ConsoleEnvironment - - protected def _createConsole( - consoleOutput: ConsoleOutput = StandardConsoleOutput - ): Console - implicit val scheduler: ScheduledExecutorService = Threading.singleThreadScheduledExecutor( loggerFactory.threadName + "-env-sched", noTracingLogger, ) - final def config: Config = currentConfig.get() + def config: CantonConfig = currentConfig.get() def pokeOrUpdateConfig( - newConfig: Option[Either[String, Config]] + newConfig: Option[Either[String, CantonConfig]] )(implicit traceContext: TraceContext): Unit = { def pokeDeclarativeApis(configState: Either[Unit, Boolean]): Unit = Seq(sequencers, mediators, participants).foreach { group => @@ -114,12 +107,9 @@ abstract class Environment[Config <: SharedCantonConfig[Config]]( } } - private val currentConfig = new AtomicReference[Config](initialConfig) + private val currentConfig = new AtomicReference[CantonConfig](initialConfig) private val histogramInventory = new HistogramInventory() private val histograms = new CantonHistograms()(histogramInventory) - val dbStorageHistograms = new DbStorageHistograms( - MetricName("cn") - )(histogramInventory) private val baseFilter = new MetricsInfoFilter( config.monitoring.metrics.globalFilters, config.monitoring.metrics.qualifiers.toSet, @@ -160,8 +150,9 @@ abstract class Environment[Config <: SharedCantonConfig[Config]]( def createConsole( consoleOutput: ConsoleOutput = StandardConsoleOutput - ): Console = { - val console = _createConsole(consoleOutput) + ): ConsoleEnvironment = { + val console = + new ConsoleEnvironment(this, consoleOutput) healthDumpGenerator .putIfAbsent(createHealthDumpGenerator(console.grpcAdminCommandRunner)) .discard @@ -634,31 +625,10 @@ object Environment { } -trait EnvironmentFactory[C <: SharedCantonConfig[C], E <: Environment[C]] { +trait EnvironmentFactory { def create( - config: C, + config: CantonConfig, loggerFactory: NamedLoggerFactory, testingConfigInternal: TestingConfigInternal = TestingConfigInternal(), - ): E -} - -class CantonEnvironment( - initialConfig: CantonConfig, - override val testingConfig: TestingConfigInternal, - participantNodeFactory: ParticipantNodeBootstrapFactory, - sequencerNodeFactory: SequencerNodeBootstrapFactory, - mediatorNodeFactory: MediatorNodeBootstrapFactory, - override val loggerFactory: NamedLoggerFactory, -) extends Environment[CantonConfig]( - initialConfig, - testingConfig, - participantNodeFactory, - sequencerNodeFactory, - mediatorNodeFactory, - loggerFactory, - ) { - - override type Console = CantonConsoleEnvironment - override def _createConsole(output: ConsoleOutput) = - new CantonConsoleEnvironment(this, output) + ): Environment } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Nodes.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Nodes.scala index eef219c62e..6d22d83da1 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Nodes.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Nodes.scala @@ -216,15 +216,13 @@ class ManagedNodes[ promise: Promise[Either[StartupError, NodeBootstrap]] ): EitherT[Future, StartupError, NodeBootstrap] = { val params = parametersFor(name) - - val instanceCreated = create(name, config) - val startup = for { // start migration _ <- EitherT(Future(checkMigration(name, config.storage, params))) instance = { - nodes.put(name, StartingUp(promise, instanceCreated)).discard - instanceCreated + val instance = create(name, config) + nodes.put(name, StartingUp(promise, instance)).discard + instance } declarativeHandler <- instance diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala deleted file mode 100644 index 0c3354b80d..0000000000 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton - -import config.CantonConfig - -package object environment { - type CantonEnvironmentFactory = EnvironmentFactory[CantonConfig, CantonEnvironment] -} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/metrics/MetricsRegistry.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/metrics/MetricsRegistry.scala index 3b443e1759..56cb2048a1 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/metrics/MetricsRegistry.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/metrics/MetricsRegistry.scala @@ -28,7 +28,6 @@ import io.opentelemetry.instrumentation.runtimetelemetry.internal.Experimental import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder import io.opentelemetry.sdk.metrics.`export`.{MetricExporter, MetricReader, PeriodicMetricReader} import io.opentelemetry.sdk.metrics.internal.state.MetricStorage -import org.slf4j.helpers.NOPLogger import java.io.File import java.util.concurrent.ScheduledExecutorService @@ -246,8 +245,8 @@ final case class MetricsRegistry( .registered() .map(_.name.toString()) .toSet, - // TODO(#13956) - port this in a smarter way to canton - onlyLogMissingHistograms = Some(NOPLogger.NOP_LOGGER), + onlyLogMissingHistograms = + if (testingSupportAdhocMetrics) Some(logger.underlying) else None, globalMetricsContext = extraContext, ), baseFilter, diff --git a/canton/community/app/src/main/resources/logback.xml b/canton/community/app/src/main/resources/logback.xml index d4ecd5c77f..b826a63d1a 100644 --- a/canton/community/app/src/main/resources/logback.xml +++ b/canton/community/app/src/main/resources/logback.xml @@ -56,7 +56,7 @@ - ${LOG_FILE_NAME:-log/canton_network.clog} + ${LOG_FILE_NAME:-log/canton.log} ${LOG_FILE_APPEND:-true} ${LOG_IMMEDIATE_FLUSH:-true} @@ -70,7 +70,7 @@ - ${LOG_FILE_NAME:-log/canton_network.clog} + ${LOG_FILE_NAME:-log/canton.log} ${LOG_FILE_APPEND:-true} ${LOG_IMMEDIATE_FLUSH:-true} @@ -88,13 +88,13 @@ - ${LOG_FILE_NAME:-log/canton_network.clog} + ${LOG_FILE_NAME:-log/canton.log} true ${LOG_IMMEDIATE_FLUSH:-true} - ${LOG_FILE_NAME:-log/canton_network.clog}.%d{${LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz + ${LOG_FILE_NAME:-log/canton.log}.%d{${LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz ${LOG_FILE_HISTORY:-12} @@ -108,13 +108,13 @@ - ${LOG_FILE_NAME:-log/canton_network.clog} + ${LOG_FILE_NAME:-log/canton.log} true ${LOG_IMMEDIATE_FLUSH:-true} - ${LOG_FILE_NAME:-log/canton_network.clog}.%d{${LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz + ${LOG_FILE_NAME:-log/canton.log}.%d{${LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz ${LOG_FILE_HISTORY:-12} diff --git a/canton/community/app/src/main/resources/repl/banner.txt b/canton/community/app/src/main/resources/repl/banner.txt index ae9591f841..0a40281ea0 100644 --- a/canton/community/app/src/main/resources/repl/banner.txt +++ b/canton/community/app/src/main/resources/repl/banner.txt @@ -1,10 +1,10 @@ - ______ __ _ __ __ __ - / ____/____ _ ____ / /_ ____ ____ / | / /___ / /_ _ __ ____ _____ / /__ - / / / __ `// __ \ / __// __ \ / __ \ / |/ // _ \ / __/| | /| / // __ \ / ___// //_/ -/ /___ / /_/ // / / // /_ / /_/ // / / / / /| // __// /_ | |/ |/ // /_/ // / / ,< -\____/ \__,_//_/ /_/ \__/ \____//_/ /_/ /_/ |_/ \___/ \__/ |__/|__/ \____//_/ /_/|_| + _____ _ + / ____| | | + | | __ _ _ __ | |_ ___ _ __ + | | / _` | '_ \| __/ _ \| '_ \ + | |___| (_| | | | | || (_) | | | | + \_____\__,_|_| |_|\__\___/|_| |_| - - Welcome to the Canton Network node console! + Welcome to Canton! Type `help` to get started. `exit` to leave. diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala index c73b474917..243b8a1421 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala @@ -19,15 +19,15 @@ import com.digitalasset.canton.config.{ DefaultPorts, GCLoggingConfig, Generate, - SharedCantonConfig, } import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.environment.{Environment, EnvironmentFactory} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.{NoTracing, TraceContext} import com.digitalasset.canton.util.JarResourceUtils +import com.digitalasset.canton.version.ReleaseVersion import com.sun.management.GarbageCollectionNotificationInfo -import com.typesafe.config.ConfigFactory +import com.typesafe.config.{Config, ConfigFactory} import org.slf4j.LoggerFactory import java.lang.management.ManagementFactory @@ -44,12 +44,10 @@ import scala.util.control.NonFatal */ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { - type Config <: SharedCantonConfig[Config] - type E <: Environment[Config] + protected def environmentFactory: EnvironmentFactory - protected def environmentFactory: EnvironmentFactory[Config, E] - - protected def withManualStart(config: Config): Config + protected def withManualStart(config: CantonConfig): CantonConfig = + config.copy(parameters = config.parameters.copy(manualStart = true)) protected def additionalVersions: Map[String, String] = Map.empty @@ -63,8 +61,6 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { Console.out.println(s"$name: $version") } - protected def logAppVersion(): Unit = logger.info(s"Starting Canton version ${BuildInfo.version}") - // BE CAREFUL: Set the environment variables before you touch anything related to // logback as otherwise, the logback configuration will be read without these // properties being considered @@ -101,8 +97,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { case (None, _) => } - logAppVersion() - + logger.info(s"Starting Canton version ${ReleaseVersion.current}") if (cliOptions.logTruncate) { cliOptions.logFileAppender match { case LogFileAppender.Rolling => @@ -118,7 +113,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { // Canton does not die on a warning status. logbackStatusManager.remove(killingStatusListener) - private val environmentRef: AtomicReference[Option[E]] = new AtomicReference(None) + private val environmentRef: AtomicReference[Option[Environment]] = new AtomicReference(None) sys.runtime.addShutdownHook(new Thread(() => { try { logger.info("Shutting down...") @@ -172,7 +167,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { def loadConfigFromFiles( loggingString: String = "Starting up with resolved config" - )(implicit traceContext: TraceContext): Either[CantonConfigError, Config] = { + )(implicit traceContext: TraceContext): Either[CantonConfigError, CantonConfig] = { val mergedUserConfigsE = NonEmpty.from(configFiles) match { case None if cliOptions.configMap.isEmpty => Left(ConfigErrors.NoConfigFiles.Error()) @@ -217,7 +212,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { sys.exit(1) } - private def writeConfigToTmpFile(mergedUserConfigs: com.typesafe.config.Config) = { + private def writeConfigToTmpFile(mergedUserConfigs: Config) = { val tmp = File.newTemporaryFile("canton-config-error-", ".conf") logger.error( s"An error occurred after parsing a config file that was obtained by merging multiple config " + @@ -294,11 +289,11 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { } def loadConfig( - config: com.typesafe.config.Config, + config: Config, defaultPorts: Option[DefaultPorts], - ): Either[CantonConfigError, Config] + ): Either[CantonConfigError, CantonConfig] - protected[this] def startupConfigFileMonitoring(environment: E): Unit = + private def startupConfigFileMonitoring(environment: Environment): Unit = TraceContext.withNewTraceContext("config_file_monitoring") { implicit traceContext => def modificationTimestamp(): Long = Config.configFiles.map(_.lastModified()).foldLeft(0L) { case (acc, item) => diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala index 7a1cd8c8f1..b64820db89 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala @@ -1,31 +1,21 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package com.digitalasset.canton import com.digitalasset.canton.config.ConfigErrors.CantonConfigError import com.digitalasset.canton.config.{CantonConfig, DefaultPorts} -import com.digitalasset.canton.environment.{ - CantonEnvironment, - CommunityEnvironmentFactory, - EnvironmentFactory, -} +import com.digitalasset.canton.environment.{CommunityEnvironmentFactory, EnvironmentFactory} +import com.typesafe.config.Config object CantonCommunityApp extends CantonAppDriver { - override type Config = CantonConfig - - override type E = CantonEnvironment - override def loadConfig( - config: com.typesafe.config.Config, + config: Config, defaultPorts: Option[DefaultPorts], ): Either[CantonConfigError, CantonConfig] = CantonConfig.loadAndValidate(config, defaultPorts) - override protected def environmentFactory: EnvironmentFactory[Config, E] = + override protected def environmentFactory: EnvironmentFactory = CommunityEnvironmentFactory - - override def withManualStart(config: CantonConfig): CantonConfig = - config.copy(parameters = config.parameters.copy(manualStart = true)) } diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala index 9d66ca00e5..7e58292b38 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala @@ -19,10 +19,10 @@ import scala.util.control.NonFatal */ trait Runner extends NamedLogging { - def run(environment: Environment[?]): Unit + def run(environment: Environment): Unit // TODO(#24954): Convert to using declarative api, when it becomes available - def uploadDar(environment: Environment[?])(darPath: String): Unit = { + def uploadDar(environment: Environment)(darPath: String): Unit = { val consoleEnvironment = environment.createConsole() consoleEnvironment.participants.local .flatMap(_.underlying) @@ -48,7 +48,7 @@ class ServerRunner( ) extends Runner with NoTracing { - def run(environment: Environment[?]): Unit = + def run(environment: Environment): Unit = try { def start(): Unit = environment @@ -86,7 +86,7 @@ class ConsoleInteractiveRunner( override val loggerFactory: NamedLoggerFactory, dars: Seq[String] = Seq.empty, ) extends Runner { - def run(environment: Environment[?]): Unit = { + def run(environment: Environment): Unit = { val success = try { val consoleEnvironment = environment.createConsole() @@ -100,9 +100,7 @@ class ConsoleInteractiveRunner( }, ) } catch { - case NonFatal(e) => - logger.error(e.getMessage)(TraceContext.empty) - false + case NonFatal(_) => false } sys.exit(if (success) 0 else 1) } @@ -115,7 +113,7 @@ class ConsoleScriptRunner( private val Ok = 0 private val Error = 1 - override def run(environment: Environment[?]): Unit = { + override def run(environment: Environment): Unit = { val exitCode = ConsoleScriptRunner.run(environment, scriptPath, logger) match { case Right(_unit) => @@ -189,14 +187,14 @@ object ConsoleScriptRunner extends NoTracing { new ConsoleScriptRunner(CantonScriptFromFile(scriptPath), loggerFactory) def run( - environment: Environment[?], + environment: Environment, scriptPath: File, logger: TracedLogger, ): Either[HeadlessConsole.HeadlessConsoleError, Unit] = run(environment, CantonScriptFromFile(scriptPath), logger) def run( - environment: Environment[?], + environment: Environment, cantonScript: CantonScript, logger: TracedLogger, ): Either[HeadlessConsole.HeadlessConsoleError, Unit] = { diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala index e25ef9f34e..059ff67f0c 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala @@ -21,7 +21,7 @@ object Generate { ) } - def process(command: Command.Generate.Target, config: SharedCantonConfig[?]): Unit = + def process(command: Command.Generate.Target, config: CantonConfig): Unit = command match { case Command.Generate.RemoteConfig => val writers = new CantonConfig.ConfigWriters(confidential = false) diff --git a/canton/community/app/src/pack/examples/01-simple-topology/README.md b/canton/community/app/src/pack/examples/01-simple-topology/README.md index eb0d66b345..97a0c9530c 100644 --- a/canton/community/app/src/pack/examples/01-simple-topology/README.md +++ b/canton/community/app/src/pack/examples/01-simple-topology/README.md @@ -1,7 +1,7 @@ # Simple Topology Example The simple topology example features a simple setup, starting two participants named `participant1` -and `participant2`, and a domain named `mydomain` in a single process. +and `participant2`, and a synchronizer named `da` in a single process. How to run the example is featured in the [getting started tutorial]( https://docs.daml.com/canton/tutorials/getting_started.html#starting-canton). diff --git a/canton/community/app/src/pack/examples/04-high-availability/run-docker-postgres.sh b/canton/community/app/src/pack/examples/04-high-availability/run-docker-postgres.sh index b8eca889d5..df9d5f1413 100755 --- a/canton/community/app/src/pack/examples/04-high-availability/run-docker-postgres.sh +++ b/canton/community/app/src/pack/examples/04-high-availability/run-docker-postgres.sh @@ -6,5 +6,5 @@ docker run --name postgres --rm \ -e POSTGRES_PASSWORD=supersafe \ -p 5432:5432 \ -v "$PWD/postgres-docker-initdb.sh:/docker-entrypoint-initdb.d/postgres-docker-initdb.sh" \ - postgres:18 + postgres:17 diff --git a/canton/community/app/src/pack/examples/05-composability/README.md b/canton/community/app/src/pack/examples/05-composability/README.md index b69c27a21d..3f63c1e66e 100644 --- a/canton/community/app/src/pack/examples/05-composability/README.md +++ b/canton/community/app/src/pack/examples/05-composability/README.md @@ -1,7 +1,7 @@ # Composability Example -The composability example features a workflow that spans across two domains. -It starts two domains and five participants in a single process. +The composability example features a workflow that spans across two synchronizers. +It starts two synchronizers and five participants in a single process. The details are described in the [composability tutorial]( https://docs.daml.com/canton/tutorials/composability.html). diff --git a/canton/community/app/src/pack/examples/07-repair/README.md b/canton/community/app/src/pack/examples/07-repair/README.md index c9f1aca7fb..9617819822 100644 --- a/canton/community/app/src/pack/examples/07-repair/README.md +++ b/canton/community/app/src/pack/examples/07-repair/README.md @@ -1,38 +1,21 @@ # Repair Examples The repair example features multiple topologies for repair-based tutorials, each with different sets of participants -and domains depending on the demonstrated capability. +and synchronizers depending on the demonstrated capability. -## 1. [Recovering from a broken Domain](https://docs.daml.com/canton/usermanual/repairing.html#recovering-from-a-lost-domain) +## 1. Recovering from a broken Synchronizer depends on files: - Participant configurations: participant1.conf and participant2.conf -- Domain configurations: domain-repair-lost.conf and domain-repair-new.conf +- Synchronizer configurations: synchronizer-repair-lost.conf and synchronizer-repair-new.conf - enable-preview-commands.conf to enable "preview" and "repair" commands -- Initialization script: domain-repair-init.canton that populates the participants and "lostDomain" with Iou contracts +- Initialization script: synchronizer-repair-init.canton that populates the participants and "lostSynchronizer" with Iou contracts To set up this scenario, run ``` ../../bin/canton -Dcanton-examples.dar-path=../../dars/CantonExamples.dar \ - -c participant1.conf,participant2.conf,domain-repair-lost.conf,domain-repair-new.conf \ + -c participant1.conf,participant2.conf,synchronizer-repair-lost.conf,synchronizer-repair-new.conf \ -c ../../config/storage/h2.conf,enable-preview-commands.conf \ - --bootstrap domain-repair-init.canton -``` - -## 2. [Importing contracts to Canton](https://docs.daml.com/canton/usermanual/repairing.html#importing-existing-contracts) - -depends on files: -- Participant configurations: participant1.conf, participant2.conf, participant3.conf for the "import ledger", and participant4.conf for the "export ledger" -- Domain configurations: domain-export-ledger.conf and domain-import-ledger.conf for the export and import ledgers respectively -- enable-preview-commands.conf to enable "preview" and "repair" commands -- Initialization script: import-ledger-init.canton that populates the export ledger with Paint agreement and Iou contracts - -To set up this scenario, run - -``` - ../../bin/canton -Dcanton-examples.dar-path=../../dars/CantonExamples.dar \ - -c participant1.conf,participant2.conf,participant3.conf,participant4.conf,domain-export-ledger.conf,domain-import-ledger.conf \ - -c ../../config/storage/h2.conf,enable-preview-commands.conf \ - --bootstrap import-ledger-init.canton + --bootstrap synchronizer-repair-init.canton ``` diff --git a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json b/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json index 576d0b3589..cff4ab26b2 100644 --- a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json +++ b/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering-performance.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 8, + "id": 5, "links": [], "liveNow": false, "panels": [ @@ -94,7 +94,7 @@ "overrides": [] }, "gridPos": { - "h": 12, + "h": 7, "w": 12, "x": 0, "y": 1 @@ -105,10 +105,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -119,8 +119,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -187,7 +187,7 @@ "overrides": [] }, "gridPos": { - "h": 12, + "h": 7, "w": 12, "x": 12, "y": 1 @@ -198,10 +198,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -212,8 +212,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node, ordering_stage) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"batch-availability-total\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"batch-availability-total\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -281,10 +281,10 @@ "overrides": [] }, "gridPos": { - "h": 17, + "h": 7, "w": 8, "x": 0, - "y": 13 + "y": 8 }, "id": 107, "options": { @@ -299,7 +299,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -310,10 +310,22 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "sum by (namespace, job, node, is_block_empty) (irate(daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))", - "legendFormat": "{{namespace}}.{{node}} (empty: {{is_block_empty}})", + "expr": "sum(irate(daml_sequencer_bftordering_global_ordered_blocks_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval]))", + "legendFormat": "All blocks -", "range": true, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(daml_sequencer_bftordering_global_ordered_blocks_total{reporting_sequencer=\"$reporting_sequencer\", is_block_empty=\"true\"}[$__rate_interval])", + "hide": false, + "legendFormat": "Empty blocks -", + "range": true, + "refId": "B" } ], "title": "Throughput - blocks/s", @@ -379,10 +391,10 @@ "overrides": [] }, "gridPos": { - "h": 17, + "h": 7, "w": 8, "x": 8, - "y": 13 + "y": 8 }, "id": 108, "options": { @@ -397,7 +409,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -408,9 +420,9 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_global_ordered_batches_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", + "expr": "irate(daml_sequencer_bftordering_global_ordered_batches_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", "interval": "", - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "Batches/s", "range": true, "refId": "A" } @@ -477,10 +489,10 @@ "overrides": [] }, "gridPos": { - "h": 17, + "h": 7, "w": 8, "x": 16, - "y": 13 + "y": 8 }, "id": 109, "options": { @@ -495,7 +507,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -507,8 +519,8 @@ }, "editorMode": "code", "exemplar": false, - "expr": "sum by (namespace, job, node) (irate(daml_sequencer_bftordering_global_ordered_requests_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "sum(irate(daml_sequencer_bftordering_global_ordered_requests_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval]))", + "legendFormat": "Requests/s -", "range": true, "refId": "A" } @@ -522,7 +534,7 @@ "h": 1, "w": 24, "x": 0, - "y": 30 + "y": 15 }, "id": 12, "panels": [ @@ -571,8 +583,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -588,7 +599,7 @@ "h": 8, "w": 12, "x": 0, - "y": 31 + "y": 10 }, "id": 11, "options": { @@ -596,10 +607,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -610,8 +621,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by(namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-PekkoP2PGrpcConnectionManagingActor\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"module-queue-PekkoP2PGrpcConnectionManagingActor\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -664,8 +675,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" } ] } @@ -676,7 +686,7 @@ "h": 8, "w": 12, "x": 12, - "y": 31 + "y": 10 }, "id": 103, "options": { @@ -684,10 +694,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -698,8 +708,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "sum by (namespace, job, node) (daml_sequencer_bftordering_performance_moduleQueueSize_PekkoP2PGrpcConnectionManagingActor{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"})", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_PekkoP2PGrpcConnectionManagingActor{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -753,8 +763,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -770,7 +779,7 @@ "h": 8, "w": 24, "x": 0, - "y": 39 + "y": 18 }, "id": 13, "options": { @@ -778,10 +787,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -792,8 +801,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node, source_sequencer) (rate(daml_sequencer_bftordering_p2p_send_grpc_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{source_sequencer}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_p2p_send_grpc_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -846,8 +855,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -863,7 +871,7 @@ "h": 8, "w": 12, "x": 0, - "y": 47 + "y": 26 }, "id": 17, "options": { @@ -871,7 +879,7 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "single", @@ -885,8 +893,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_p2p_send_network_write_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_p2p_send_network_write_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -939,8 +947,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -956,7 +963,7 @@ "h": 8, "w": 12, "x": 12, - "y": 47 + "y": 26 }, "id": 15, "options": { @@ -964,7 +971,7 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "single", @@ -978,8 +985,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by(namespace, job, node) (rate(daml_sequencer_bftordering_p2p_receive_processing_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_p2p_receive_processing_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -997,7 +1004,7 @@ "h": 1, "w": 24, "x": 0, - "y": 31 + "y": 16 }, "id": 7, "panels": [ @@ -1046,8 +1053,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1063,7 +1069,7 @@ "h": 6, "w": 12, "x": 0, - "y": 32 + "y": 11 }, "id": 18, "options": { @@ -1071,10 +1077,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1085,8 +1091,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-mempool\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"module-queue-mempool\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1139,8 +1145,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1155,7 +1160,7 @@ "h": 6, "w": 12, "x": 12, - "y": 32 + "y": 11 }, "id": 101, "options": { @@ -1163,10 +1168,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1177,8 +1182,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_mempool{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_mempool{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1231,8 +1236,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1248,7 +1252,7 @@ "h": 6, "w": 12, "x": 0, - "y": 38 + "y": 17 }, "id": 10, "options": { @@ -1256,10 +1260,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1270,8 +1274,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-availability\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"module-queue-availability\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1324,8 +1328,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1340,7 +1343,7 @@ "h": 6, "w": 12, "x": 12, - "y": 38 + "y": 17 }, "id": 102, "options": { @@ -1348,10 +1351,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1362,8 +1365,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_availability{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_availability{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1417,8 +1420,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1434,7 +1436,7 @@ "h": 6, "w": 12, "x": 0, - "y": 44 + "y": 23 }, "id": 24, "options": { @@ -1442,10 +1444,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1456,8 +1458,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"batch-wait\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"batch-wait\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1511,8 +1513,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1528,7 +1529,7 @@ "h": 6, "w": 12, "x": 12, - "y": 44 + "y": 23 }, "id": 21, "options": { @@ -1536,10 +1537,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1550,8 +1551,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"batch-dissemination-total\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"batch-dissemination-total\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1605,8 +1606,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1622,7 +1622,7 @@ "h": 6, "w": 12, "x": 0, - "y": 50 + "y": 29 }, "id": 23, "options": { @@ -1630,10 +1630,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1644,8 +1644,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"batch-queued-for-block-inclusion\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"batch-queued-for-block-inclusion\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1699,8 +1699,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1716,7 +1715,7 @@ "h": 6, "w": 12, "x": 12, - "y": 50 + "y": 29 }, "id": 3, "options": { @@ -1724,10 +1723,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1738,8 +1737,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_availability_batch_regressions{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}, {{stage}}", + "expr": "irate(daml_sequencer_bftordering_availability_batch_regressions{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1792,8 +1791,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1809,7 +1807,7 @@ "h": 6, "w": 12, "x": 0, - "y": 56 + "y": 35 }, "id": 4, "options": { @@ -1817,10 +1815,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1831,8 +1829,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"availability-hash-batchId\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"availability-hash-batchId\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1885,8 +1883,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1902,7 +1899,7 @@ "h": 6, "w": 12, "x": 12, - "y": 56 + "y": 35 }, "id": 5, "options": { @@ -1910,10 +1907,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1924,8 +1921,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"availability-sign-local-batchId\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"availability-sign-local-batchId\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1979,8 +1976,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1996,7 +1992,7 @@ "h": 6, "w": 12, "x": 0, - "y": 62 + "y": 41 }, "id": 9, "options": { @@ -2004,10 +2000,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2018,8 +2014,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"request-queued-for-batch-inclusion\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"request-queued-for-batch-inclusion\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2072,8 +2068,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2089,7 +2084,7 @@ "h": 6, "w": 12, "x": 12, - "y": 62 + "y": 41 }, "id": 6, "options": { @@ -2097,10 +2092,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2111,8 +2106,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"availability-sign-remote-batchId\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"availability-sign-remote-batchId\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2165,8 +2160,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2182,7 +2176,7 @@ "h": 6, "w": 12, "x": 0, - "y": 68 + "y": 47 }, "id": 26, "options": { @@ -2190,10 +2184,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2204,8 +2198,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"availability-sign-local-batches\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"availability-sign-local-batches\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2258,8 +2252,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2275,7 +2268,7 @@ "h": 6, "w": 12, "x": 12, - "y": 68 + "y": 47 }, "id": 8, "options": { @@ -2283,10 +2276,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2297,8 +2290,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"availability-signature-verify-ack\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"availability-signature-verify-ack\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2351,8 +2344,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2368,7 +2360,7 @@ "h": 6, "w": 12, "x": 0, - "y": 74 + "y": 53 }, "id": 82, "options": { @@ -2376,10 +2368,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2390,8 +2382,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.addBatch\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.addBatch\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2444,8 +2436,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2461,7 +2452,7 @@ "h": 6, "w": 12, "x": 12, - "y": 74 + "y": 53 }, "id": 28, "options": { @@ -2469,10 +2460,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2483,8 +2474,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.fetchBatches\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.fetchBatches\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2537,8 +2528,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2554,7 +2544,7 @@ "h": 6, "w": 12, "x": 0, - "y": 80 + "y": 59 }, "id": 22, "options": { @@ -2562,10 +2552,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2576,8 +2566,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"batch-validation\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"batch-validation\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2588,7 +2578,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Summary of cache hits, misses, and evictions for the Availability store's batch data cache", "fieldConfig": { @@ -2634,8 +2624,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" } ] }, @@ -2678,7 +2667,7 @@ "h": 6, "w": 12, "x": 12, - "y": 80 + "y": 65 }, "hideTimeOverride": false, "id": 110, @@ -2690,7 +2679,7 @@ ], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "multi", @@ -2701,43 +2690,43 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, - "editorMode": "code", - "expr": "sum by (namespace, job, node) (irate(daml_cache_hits{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\", cache=\"batch-cache\"}[$__rate_interval]))", + "editorMode": "builder", + "expr": "sum(irate(daml_cache_hits{cache=\"batch-cache\"}[$__rate_interval]))", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}} Cache Hits", + "legendFormat": "Cache Hits", "refId": "A" }, { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, - "editorMode": "code", - "expr": "sum by (namespace, job, node) (irate(daml_cache_misses{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\", cache=\"batch-cache\"}[$__rate_interval]))", + "editorMode": "builder", + "expr": "sum(irate(daml_cache_misses{cache=\"batch-cache\"}[$__rate_interval]))", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}} Cache Misses", + "legendFormat": "Cache Misses", "refId": "B" }, { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, - "editorMode": "code", - "expr": "sum by (namespace, job, node) (irate(daml_cache_evictions{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\", cache=\"batch-cache\"}[$__rate_interval]))", + "editorMode": "builder", + "expr": "sum(irate(daml_cache_evictions{cache=\"batch-cache\"}[$__rate_interval]))", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}} Cache Evictions", + "legendFormat": "Cache Evictions", "refId": "C" } ], @@ -2754,7 +2743,7 @@ "h": 1, "w": 24, "x": 0, - "y": 32 + "y": 17 }, "id": 45, "panels": [ @@ -2820,7 +2809,7 @@ "h": 7, "w": 12, "x": 0, - "y": 33 + "y": 18 }, "id": 80, "options": { @@ -2828,10 +2817,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2842,8 +2831,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-consensus\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"module-queue-consensus\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -2912,7 +2901,7 @@ "h": 7, "w": 12, "x": 12, - "y": 33 + "y": 18 }, "id": 104, "options": { @@ -2920,10 +2909,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2934,8 +2923,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_consensus{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_consensus{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3005,7 +2994,7 @@ "h": 7, "w": 12, "x": 0, - "y": 40 + "y": 25 }, "id": 81, "options": { @@ -3013,10 +3002,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3027,8 +3016,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-segment-module\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"module-queue-segment-module\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3097,7 +3086,7 @@ "h": 7, "w": 12, "x": 12, - "y": 40 + "y": 25 }, "id": 105, "options": { @@ -3105,10 +3094,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3119,8 +3108,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_segment_module{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_segment_module{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3131,7 +3120,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Block commit time at the consensus (PBFT) level.", "fieldConfig": { @@ -3185,8 +3174,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -3202,7 +3190,7 @@ "h": 7, "w": 12, "x": 0, - "y": 47 + "y": 32 }, "hideTimeOverride": true, "id": 39, @@ -3211,10 +3199,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3222,16 +3210,16 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_consensus_commit_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_consensus_commit_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3285,8 +3273,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -3302,7 +3289,7 @@ "h": 7, "w": 12, "x": 12, - "y": 47 + "y": 32 }, "id": 46, "options": { @@ -3310,10 +3297,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3324,8 +3311,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-segment-proposal-to-commit-latency\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-segment-proposal-to-commit-latency\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3395,7 +3382,7 @@ "h": 6, "w": 12, "x": 0, - "y": 54 + "y": 39 }, "id": 88, "options": { @@ -3403,10 +3390,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3417,8 +3404,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-segment-block-commit-latency\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-segment-block-commit-latency\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3488,7 +3475,7 @@ "h": 6, "w": 12, "x": 12, - "y": 54 + "y": 39 }, "id": 87, "options": { @@ -3496,10 +3483,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3510,8 +3497,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-block-proposal-wait\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-block-proposal-wait\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3581,7 +3568,7 @@ "h": 7, "w": 12, "x": 0, - "y": 60 + "y": 45 }, "id": 86, "options": { @@ -3589,10 +3576,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3603,8 +3590,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-epoch-start-wait\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-epoch-start-wait\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3674,7 +3661,7 @@ "h": 7, "w": 12, "x": 12, - "y": 60 + "y": 45 }, "id": 83, "options": { @@ -3682,10 +3669,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3696,8 +3683,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-epoch-completion-wait\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-epoch-completion-wait\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3766,7 +3753,7 @@ "h": 6, "w": 12, "x": 0, - "y": 67 + "y": 52 }, "id": 63, "options": { @@ -3774,10 +3761,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3788,8 +3775,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.startEpoch\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.startEpoch\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3858,7 +3845,7 @@ "h": 6, "w": 12, "x": 12, - "y": 67 + "y": 52 }, "id": 47, "options": { @@ -3866,10 +3853,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3880,8 +3867,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.completeEpoch\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.completeEpoch\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -3950,7 +3937,7 @@ "h": 7, "w": 12, "x": 0, - "y": 73 + "y": 58 }, "id": 64, "options": { @@ -3958,10 +3945,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3972,8 +3959,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrePrepare\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrePrepare\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4042,7 +4029,7 @@ "h": 7, "w": 12, "x": 12, - "y": 73 + "y": 58 }, "id": 50, "options": { @@ -4050,10 +4037,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4064,8 +4051,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPreparesAtomically\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addPrepares\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4134,7 +4121,7 @@ "h": 6, "w": 12, "x": 0, - "y": 80 + "y": 65 }, "id": 52, "options": { @@ -4142,10 +4129,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4156,8 +4143,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addOrderedBlockAtomically\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addOrderedBlock\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4226,7 +4213,7 @@ "h": 6, "w": 12, "x": 12, - "y": 80 + "y": 65 }, "id": 66, "options": { @@ -4234,10 +4221,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4248,8 +4235,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-signature-verify-poa-ack\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-signature-verify-poa-ack\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4318,7 +4305,7 @@ "h": 7, "w": 12, "x": 0, - "y": 86 + "y": 71 }, "id": 68, "options": { @@ -4326,10 +4313,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4340,8 +4327,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-validate-signed-message\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-validate-signed-message\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4410,7 +4397,7 @@ "h": 7, "w": 12, "x": 12, - "y": 86 + "y": 71 }, "id": 76, "options": { @@ -4418,10 +4405,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4432,8 +4419,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"sign-BftSignedConsensusMessage\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"sign-BftSignedConsensusMessage\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4451,7 +4438,7 @@ "h": 1, "w": 24, "x": 0, - "y": 33 + "y": 18 }, "id": 89, "panels": [ @@ -4500,8 +4487,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -4517,7 +4503,7 @@ "h": 7, "w": 24, "x": 0, - "y": 34 + "y": 134 }, "id": 51, "options": { @@ -4525,10 +4511,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4539,8 +4525,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_epoch_view_changes{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_epoch_view_changes{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4593,8 +4579,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -4610,7 +4595,7 @@ "h": 7, "w": 12, "x": 0, - "y": 41 + "y": 141 }, "id": 84, "options": { @@ -4618,10 +4603,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4632,8 +4617,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addViewChangeMessage\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.addViewChangeMessage\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4686,8 +4671,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -4703,7 +4687,7 @@ "h": 7, "w": 12, "x": 12, - "y": 41 + "y": 141 }, "id": 77, "options": { @@ -4711,10 +4695,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4725,8 +4709,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-postponed-view-messages-queue-latency\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-postponed-view-messages-queue-latency\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4794,7 +4778,7 @@ "h": 7, "w": 12, "x": 0, - "y": 48 + "y": 148 }, "id": 97, "options": { @@ -4805,7 +4789,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4816,8 +4800,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_postponed_view_messages_queue_size{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_postponed_view_messages_queue_size{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4885,7 +4869,7 @@ "h": 7, "w": 12, "x": 12, - "y": 48 + "y": 148 }, "id": 95, "options": { @@ -4896,7 +4880,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4907,8 +4891,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_consensus_postponed_view_messages_queue_max_size{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_consensus_postponed_view_messages_queue_max_size{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -4976,7 +4960,7 @@ "h": 7, "w": 12, "x": 0, - "y": 55 + "y": 155 }, "id": 94, "options": { @@ -4987,7 +4971,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -4998,8 +4982,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_postponed_view_messages_dropped_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_postponed_view_messages_dropped_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -5067,7 +5051,7 @@ "h": 7, "w": 12, "x": 12, - "y": 55 + "y": 155 }, "id": 100, "options": { @@ -5078,7 +5062,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5089,15 +5073,29 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_postponed_view_messages_duplicates_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_postponed_view_messages_duplicates_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Postponed view messages duplicated (rate)", "type": "timeseries" - }, + } + ], + "title": "Consensus - view change", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 90, + "panels": [ { "datasource": { "type": "prometheus", @@ -5115,7 +5113,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -5143,8 +5141,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5158,24 +5155,20 @@ }, "gridPos": { "h": 7, - "w": 12, + "w": 24, "x": 0, - "y": 62 + "y": 135 }, - "id": 112, + "id": 72, "options": { "legend": { - "calcs": [ - "min", - "mean", - "max" - ], - "displayMode": "table", + "calcs": [], + "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5186,30 +5179,15 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, daml_sequencer_bftordering_consensus_view_change_progress_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"})", - "interval": "", - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{Leader}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"consensus-validate-consensus-certificate\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Time until progress in view", + "title": "Validate state transfer consensus certificate", "type": "timeseries" - } - ], - "title": "Consensus - view change", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 34 - }, - "id": 90, - "panels": [ + }, { "datasource": { "type": "prometheus", @@ -5255,8 +5233,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5272,18 +5249,18 @@ "h": 7, "w": 12, "x": 0, - "y": 35 + "y": 142 }, - "id": 72, + "id": 91, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5294,13 +5271,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"consensus-validate-consensus-certificate\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"state-transfer-postponed-consensus-messages-queue-latency\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Validate state transfer consensus certificate", + "title": "Postponed consensus messages queue latency", "type": "timeseries" }, { @@ -5308,7 +5285,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time to transfer a given epoch from peer(s)", "fieldConfig": { "defaults": { "color": { @@ -5321,7 +5297,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -5349,16 +5325,14 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", "value": 80 } ] - }, - "unit": "s" + } }, "overrides": [] }, @@ -5366,15 +5340,15 @@ "h": 7, "w": 12, "x": 12, - "y": 80 + "y": 142 }, - "id": 111, + "id": 96, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "single", @@ -5388,42 +5362,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"state-transfer-total-epoch-transfer-latency\"}[$__rate_interval])))", - "legendFormat": "Data transfer", + "expr": "daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_size{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"state-transfer-store-epochs\"}[$__rate_interval])))", - "hide": false, - "legendFormat": "Complete + start DB query", - "range": true, - "refId": "B" - } - ], - "title": "Epoch Transfer Duration", - "transformations": [ - { - "id": "calculateField", - "options": { - "alias": "Total epoch transfer duration", - "binary": { - "left": "Data transfer", - "reducer": "sum", - "right": "Complete + start DB query" - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } } ], + "title": "Postponed consensus messages queue size", "type": "timeseries" }, { @@ -5443,7 +5388,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -5471,16 +5416,14 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", "value": 80 } ] - }, - "unit": "s" + } }, "overrides": [] }, @@ -5488,9 +5431,9 @@ "h": 7, "w": 12, "x": 0, - "y": 42 + "y": 149 }, - "id": 91, + "id": 98, "options": { "legend": { "calcs": [], @@ -5499,7 +5442,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5510,13 +5453,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"state-transfer-postponed-consensus-messages-queue-latency\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_max_size{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Postponed consensus messages queue latency", + "title": "Postponed consensus messages queue max size", "type": "timeseries" }, { @@ -5564,8 +5507,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5580,9 +5522,9 @@ "h": 7, "w": 12, "x": 12, - "y": 42 + "y": 149 }, - "id": 96, + "id": 99, "options": { "legend": { "calcs": [], @@ -5591,7 +5533,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5602,15 +5544,29 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_size{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_dropped{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Postponed consensus messages queue size", + "title": "Postponed consensus messages dropped (rate)", "type": "timeseries" - }, + } + ], + "title": "Consensus - state transfer", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 38, + "panels": [ { "datasource": { "type": "prometheus", @@ -5668,12 +5624,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 0, - "y": 49 + "y": 136 }, - "id": 98, + "id": 85, "options": { "legend": { "calcs": [], @@ -5682,7 +5638,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5693,13 +5649,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_queue_max_size{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_incoming_retransmission_requests_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Postponed consensus messages queue max size", + "title": "Incoming retransmission requests rate", "type": "timeseries" }, { @@ -5759,12 +5715,12 @@ "overrides": [] }, "gridPos": { - "h": 7, + "h": 6, "w": 12, "x": 12, - "y": 49 + "y": 136 }, - "id": 99, + "id": 40, "options": { "legend": { "calcs": [], @@ -5773,7 +5729,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5784,29 +5740,15 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_state_transfer_postponed_consensus_messages_dropped{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_outgoing_retransmission_requests_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Postponed consensus messages dropped (rate)", + "title": "Outgoing retransmission requests rate", "type": "timeseries" - } - ], - "title": "Consensus - state transfer", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 35 - }, - "id": 38, - "panels": [ + }, { "datasource": { "type": "prometheus", @@ -5852,8 +5794,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5868,9 +5809,9 @@ "h": 6, "w": 12, "x": 0, - "y": 36 + "y": 142 }, - "id": 85, + "id": 41, "options": { "legend": { "calcs": [], @@ -5879,7 +5820,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5890,13 +5831,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_incoming_retransmission_requests_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_retransmitted_messages_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Incoming retransmission requests rate", + "title": "Retransmitted messages rate", "type": "timeseries" }, { @@ -5944,8 +5885,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -5960,9 +5900,9 @@ "h": 6, "w": 12, "x": 12, - "y": 36 + "y": 142 }, - "id": 40, + "id": 42, "options": { "legend": { "calcs": [], @@ -5971,7 +5911,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -5982,13 +5922,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_outgoing_retransmission_requests_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_retransmitted_commit_certificates_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Outgoing retransmission requests rate", + "title": "Retransmitted commit certificates rate", "type": "timeseries" }, { @@ -6008,7 +5948,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -6036,15 +5976,15 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", "value": 80 } ] - } + }, + "unit": "s" }, "overrides": [] }, @@ -6052,18 +5992,18 @@ "h": 6, "w": 12, "x": 0, - "y": 42 + "y": 148 }, - "id": 41, + "id": 78, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6074,13 +6014,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_retransmitted_messages_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"sign-BftSignedRetransmissionMessage\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Retransmitted messages rate", + "title": "Sign retransmission message", "type": "timeseries" }, { @@ -6100,7 +6040,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 0, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -6128,15 +6068,15 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", "value": 80 } ] - } + }, + "unit": "s" }, "overrides": [] }, @@ -6144,191 +6084,7 @@ "h": 6, "w": 12, "x": 12, - "y": 42 - }, - "id": 42, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_retransmitted_commit_certificates_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", - "range": true, - "refId": "A" - } - ], - "title": "Retransmitted commit certificates rate", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 12, - "x": 0, - "y": 48 - }, - "id": 78, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"sign-BftSignedRetransmissionMessage\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", - "range": true, - "refId": "A" - } - ], - "title": "Sign retransmission message", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 12, - "x": 12, - "y": 48 + "y": 148 }, "id": 79, "options": { @@ -6336,10 +6092,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6350,8 +6106,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"verify-signature-BftSignedRetransmissionMessage\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"verify-signature-BftSignedRetransmissionMessage\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -6419,7 +6175,7 @@ "h": 6, "w": 12, "x": 0, - "y": 54 + "y": 154 }, "id": 43, "options": { @@ -6430,7 +6186,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6441,8 +6197,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_discarded_wrong_epoch_retransmission_responses_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_discarded_wrong_epoch_retransmission_responses_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -6510,7 +6266,7 @@ "h": 6, "w": 12, "x": 12, - "y": 54 + "y": 154 }, "id": 44, "options": { @@ -6521,7 +6277,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6532,8 +6288,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_consensus_discarded_rate_limited_retransmission_requests_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "irate(daml_sequencer_bftordering_consensus_discarded_rate_limited_retransmission_requests_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -6551,7 +6307,7 @@ "h": 1, "w": 24, "x": 0, - "y": 36 + "y": 21 }, "id": 59, "panels": [ @@ -6600,8 +6356,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6617,7 +6372,7 @@ "h": 8, "w": 12, "x": 0, - "y": 37 + "y": 46 }, "id": 48, "options": { @@ -6625,10 +6380,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6639,8 +6394,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.latestEpoch\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.latestEpoch\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -6693,8 +6448,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6710,7 +6464,7 @@ "h": 8, "w": 12, "x": 12, - "y": 37 + "y": 46 }, "id": 53, "options": { @@ -6718,10 +6472,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6732,8 +6486,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochProgress\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochProgress\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -6786,8 +6540,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -6803,7 +6556,7 @@ "h": 8, "w": 24, "x": 0, - "y": 45 + "y": 54 }, "id": 54, "options": { @@ -6811,10 +6564,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -6825,8 +6578,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadCompleteBlocks\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadCompleteBlocks\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -6844,7 +6597,7 @@ "h": 1, "w": 24, "x": 0, - "y": 37 + "y": 22 }, "id": 29, "panels": [ @@ -6893,194 +6646,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 38 - }, - "id": 55, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"module-queue-output\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", - "range": true, - "refId": "A" - } - ], - "title": "Output module queue latency", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 38 - }, - "id": 106, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_output{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "{{namespace}}.{{node}}", - "range": true, - "refId": "A" - } - ], - "title": "Output module queue size", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "Time needed to inspect requests within a block", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7096,18 +6662,18 @@ "h": 7, "w": 12, "x": 0, - "y": 45 + "y": 17 }, - "id": 36, + "id": 55, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7118,13 +6684,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"output-block-inspection\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"module-queue-output\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Block inspection", + "title": "Output module queue latency", "type": "timeseries" }, { @@ -7144,7 +6710,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, @@ -7172,16 +6738,14 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", "value": 80 } ] - }, - "unit": "s" + } }, "overrides": [] }, @@ -7189,18 +6753,18 @@ "h": 7, "w": 12, "x": 12, - "y": 45 + "y": 17 }, - "id": 30, + "id": 106, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7211,13 +6775,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"output-block-fetch-batches\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "daml_sequencer_bftordering_performance_moduleQueueSize_output{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Total block batches fetch time", + "title": "Output module queue size", "type": "timeseries" }, { @@ -7225,6 +6789,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "Time needed to inspect requests within a block", "fieldConfig": { "defaults": { "color": { @@ -7281,18 +6846,18 @@ "h": 7, "w": 12, "x": 0, - "y": 52 + "y": 24 }, - "id": 37, + "id": 36, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7303,13 +6868,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertBlockIfMissing\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"output-block-inspection\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Insert block", + "title": "Block inspection", "type": "timeseries" }, { @@ -7373,9 +6938,9 @@ "h": 7, "w": 12, "x": 12, - "y": 52 + "y": 24 }, - "id": 34, + "id": 30, "options": { "legend": { "calcs": [], @@ -7384,7 +6949,7 @@ "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7395,13 +6960,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_topology_query_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"output-block-fetch-batches\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Query topology", + "title": "Total block batches fetch time", "type": "timeseries" }, { @@ -7465,18 +7030,18 @@ "h": 7, "w": 12, "x": 0, - "y": 59 + "y": 31 }, - "id": 31, + "id": 37, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7487,13 +7052,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertLeaderSelectionPolicyState\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertBlockIfMissing\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Save blacklist leader selection policy state", + "title": "Insert block", "type": "timeseries" }, { @@ -7557,18 +7122,18 @@ "h": 7, "w": 12, "x": 12, - "y": 59 + "y": 31 }, - "id": 32, + "id": 34, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7579,13 +7144,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertEpochIfMissing\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_topology_query_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Insert epoch metadata", + "title": "Query topology", "type": "timeseries" }, { @@ -7633,8 +7198,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7650,18 +7214,18 @@ "h": 7, "w": 12, "x": 0, - "y": 66 + "y": 38 }, - "id": 113, + "id": 31, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7672,29 +7236,15 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"output-backpressure\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"insertLeaderSelectionPolicyState\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Sequencer core backpressure duration", + "title": "Save blacklist leader selection policy state", "type": "timeseries" - } - ], - "title": "Output", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 38 - }, - "id": 60, - "panels": [ + }, { "datasource": { "type": "prometheus", @@ -7740,8 +7290,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7754,21 +7303,21 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 39 + "h": 7, + "w": 12, + "x": 12, + "y": 38 }, - "id": 35, + "id": 32, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7779,15 +7328,29 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochInfo\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.insertEpochIfMissing\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Load epoch info", + "title": "Insert epoch metadata", "type": "timeseries" - }, + } + ], + "title": "Output", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 60, + "panels": [ { "datasource": { "type": "prometheus", @@ -7833,8 +7396,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7850,18 +7412,18 @@ "h": 8, "w": 24, "x": 0, - "y": 47 + "y": 18 }, - "id": 56, + "id": 35, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7872,35 +7434,20 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadOrderedBlocks\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadEpochInfo\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Load ordered blocks", + "title": "Load epoch info", "type": "timeseries" - } - ], - "title": "Output - CFT", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 39 - }, - "id": 62, - "panels": [ + }, { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Time to compute the pruning point, based on retention factors and current time", "fieldConfig": { "defaults": { "color": { @@ -7941,8 +7488,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -7955,21 +7501,21 @@ "overrides": [] }, "gridPos": { - "h": 7, - "w": 12, + "h": 8, + "w": 24, "x": 0, - "y": 40 + "y": 26 }, - "id": 25, + "id": 56, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -7980,21 +7526,34 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"pruning-compute-pruning-point\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.loadOrderedBlocks\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Compute pruning point", + "title": "Load ordered blocks", "type": "timeseries" - }, + } + ], + "title": "Output - CFT", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 62, + "panels": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Time to evict unordered, but expired, batches in the availability DB", "fieldConfig": { "defaults": { "color": { @@ -8035,8 +7594,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8051,19 +7609,19 @@ "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 40 + "x": 0, + "y": 19 }, - "id": 27, + "id": 25, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -8074,13 +7632,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.gc\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.prune\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Availability DB - evict expired batches", + "title": "Availability DB pruning", "type": "timeseries" }, { @@ -8088,7 +7646,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time to create new table partitions, when necessary", "fieldConfig": { "defaults": { "color": { @@ -8129,8 +7686,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8145,19 +7701,19 @@ "gridPos": { "h": 7, "w": 12, - "x": 0, - "y": 47 + "x": 12, + "y": 19 }, - "id": 61, + "id": 27, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -8168,13 +7724,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.PartitionCreatorImpl.createPartitionsIfNeeded\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.availability.data.db.DbAvailabilityStore.gc\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Create partitions", + "title": "Availability DB - evict expired batches", "type": "timeseries" }, { @@ -8182,7 +7738,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time to drop table partitions that can be pruned", "fieldConfig": { "defaults": { "color": { @@ -8223,8 +7778,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8239,19 +7793,19 @@ "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 47 + "x": 0, + "y": 26 }, - "id": 67, + "id": 57, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -8262,13 +7816,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.PartitionPrunerImpl.prune\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.consensus.iss.data.db.DbEpochStore.prune\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Prune (Delete partitions)", + "title": "Consensus DB pruning", "type": "timeseries" }, { @@ -8276,7 +7830,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Time to start the Partition Manager, which may include creating new partitions", "fieldConfig": { "defaults": { "color": { @@ -8317,8 +7870,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -8334,18 +7886,18 @@ "h": 7, "w": 12, "x": 12, - "y": 47 + "y": 26 }, - "id": 69, + "id": 61, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -8356,13 +7908,13 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.pruning.PartitionManager.create\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_performance_ordering_stage_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\", ordering_stage=~\"com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.data.db.DbOutputMetadataStore.prune\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Partition manager startup", + "title": "Output DB pruning", "type": "timeseries" } ], @@ -8377,98 +7929,49 @@ "templating": { "list": [ { - "allValue": ".*", "current": { "selected": false, - "text": "All", - "value": "$__all" - }, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "definition": "label_values(daml_health_status,namespace}", - "hide": 0, - "includeAll": true, - "multi": true, - "name": "namespace", - "label": "Namespace", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(daml_health_status,namespace)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 2, - "regex": "", - "skipUrlSync": false, - "sort": 7, - "type": "query" - }, - { - "allValue": ".*", - "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] + "text": "Prometheus", + "value": "Prometheus" }, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "definition": "label_values(daml_health_status{namespace=~\"$namespace\"},job)", "hide": 0, - "includeAll": true, - "multi": true, - "name": "job", - "label": "Job", + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "DS", "options": [], - "query": { - "qryType": 1, - "query": "label_values(daml_health_status{namespace=~\"$namespace\"},job)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 2, + "query": "prometheus", + "queryValue": "", + "refresh": 1, "regex": "", "skipUrlSync": false, - "sort": 7, - "type": "query" + "type": "datasource" }, { - "allValue": ".*", "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] + "selected": false, + "text": "None", + "value": "" }, "datasource": { "type": "prometheus", "uid": "prometheus" }, - "definition": "label_values(daml_health_status{namespace=~\"$namespace\", job=~\"$job\"},node)", + "definition": "label_values(reporting_sequencer)", "hide": 0, - "includeAll": true, - "multi": true, - "name": "node", - "label": "Node", + "includeAll": false, + "label": " Reporting sequencer", + "multi": false, + "name": "reporting_sequencer", "options": [], "query": { - "qryType": 1, - "query": "label_values(daml_health_status{namespace=~\"$namespace\", job=~\"$job\"},node)", + "query": "label_values(reporting_sequencer)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, - "refresh": 2, + "refresh": 1, "regex": "", "skipUrlSync": false, - "sort": 7, + "sort": 0, "type": "query" }, { diff --git a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json b/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json index 847a1c66cc..0134cbd1d4 100644 --- a/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json +++ b/canton/community/app/src/pack/examples/13-observability/grafana/dashboards/Canton/bft-ordering.json @@ -19,7 +19,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 8, + "id": 4, "links": [], "liveNow": false, "panels": [ @@ -52,7 +52,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "ISS proceeds in epochs with stable leader assignments to \"segments\" and stable topology (i.e., sequencers can be added and remove only at epoch boundaries and leaders are reassigned to segments also at epoch boundaries). This is the current epoch.", "fieldConfig": { @@ -76,7 +76,7 @@ "overrides": [] }, "gridPos": { - "h": 11, + "h": 3, "w": 5, "x": 0, "y": 1 @@ -104,14 +104,13 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_consensus_epoch{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_consensus_epoch{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } @@ -122,7 +121,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Size in blocks of the current epoch.", "fieldConfig": { @@ -146,7 +145,7 @@ "overrides": [] }, "gridPos": { - "h": 11, + "h": 3, "w": 3, "x": 5, "y": 1 @@ -174,14 +173,13 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_consensus_epoch_length{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_consensus_epoch_length{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } @@ -192,7 +190,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Number of active validators", "fieldConfig": { @@ -216,7 +214,7 @@ "overrides": [] }, "gridPos": { - "h": 11, + "h": 3, "w": 3, "x": 8, "y": 1 @@ -244,14 +242,13 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_topology_validators{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_topology_validators{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } @@ -262,7 +259,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Number of connected and authenticated P2P peers for the selected sequencer. It should be > 2/3 at all times.", "fieldConfig": { @@ -295,8 +292,8 @@ "overrides": [] }, "gridPos": { - "h": 11, - "w": 13, + "h": 3, + "w": 3, "x": 11, "y": 1 }, @@ -320,43 +317,43 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_p2p_connections_connected{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_p2p_connections_connected{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "instant": true, "interval": "30s", "intervalFactor": 1, - "legendFormat": "Connected ({{namespace}}.{{node}})", + "legendFormat": "Connected", "range": false, "refId": "A" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_p2p_connections_authenticated{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_p2p_connections_authenticated{reporting_sequencer=\"$reporting_sequencer\"}", "hide": false, "instant": true, - "legendFormat": "Authenticated ({{namespace}}.{{node}})", + "legendFormat": "Authenticated", "range": false, "refId": "B" } ], - "title": "Peers count", + "title": "Peers", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "", + "description": "Latest block number ordered and available for the read path. When there is no traffic, a block can be empty.", "fieldConfig": { "defaults": { "color": { @@ -378,13 +375,13 @@ "overrides": [] }, "gridPos": { - "h": 12, - "w": 3, - "x": 0, - "y": 12 + "h": 3, + "w": 5, + "x": 14, + "y": 1 }, "hideTimeOverride": false, - "id": 99, + "id": 4, "maxDataPoints": 100, "options": { "colorMode": "value", @@ -406,27 +403,27 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_topology_max_tolerated_faults{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "sum(daml_sequencer_bftordering_global_ordered_blocks_total{reporting_sequencer=\"$reporting_sequencer\"})", "format": "time_series", + "hide": false, + "instant": true, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", - "range": true, "refId": "A" } ], - "title": "Max tolerated faults", + "title": "Total Output Blocks", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "The size of a weak quorum or non-faulty nodes, needed e.g. to disseminate batches", + "description": "Total number of requests ordered and output to the read path.", "fieldConfig": { "defaults": { "color": { @@ -448,13 +445,13 @@ "overrides": [] }, "gridPos": { - "h": 12, - "w": 3, - "x": 3, - "y": 12 + "h": 3, + "w": 5, + "x": 19, + "y": 1 }, "hideTimeOverride": false, - "id": 100, + "id": 40, "maxDataPoints": 100, "options": { "colorMode": "value", @@ -476,27 +473,26 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_topology_weak_quorum{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "histogram_sum(sum(daml_sequencer_bftordering_output_block_size_requests{reporting_sequencer=\"$reporting_sequencer\"}))", "format": "time_series", "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Weak quorum", + "title": "Total Output Requests", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "The size of a strong quorum, needed for ordering consensus", + "description": "", "fieldConfig": { "defaults": { "color": { @@ -518,13 +514,13 @@ "overrides": [] }, "gridPos": { - "h": 12, - "w": 3, - "x": 6, - "y": 12 + "h": 3, + "w": 8, + "x": 0, + "y": 4 }, "hideTimeOverride": false, - "id": 101, + "id": 99, "maxDataPoints": 100, "options": { "colorMode": "value", @@ -546,27 +542,26 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_topology_strong_quorum{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_topology_max_tolerated_faults{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Strong quorum", + "title": "Max tolerated faults", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Latest block number ordered and available for the read path. When there is no traffic, a block can be empty.", + "description": "The size of a weak quorum or non-faulty nodes, needed e.g. to disseminate batches", "fieldConfig": { "defaults": { "color": { @@ -588,13 +583,13 @@ "overrides": [] }, "gridPos": { - "h": 12, - "w": 7, - "x": 9, - "y": 12 + "h": 3, + "w": 8, + "x": 8, + "y": 4 }, "hideTimeOverride": false, - "id": 4, + "id": 100, "maxDataPoints": 100, "options": { "colorMode": "value", @@ -616,28 +611,26 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "sum by(namespace, job, node) (daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\"})", + "expr": "daml_sequencer_bftordering_topology_weak_quorum{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", - "hide": false, - "instant": true, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "range": true, "refId": "A" } ], - "title": "Total Output Blocks", + "title": "Weak quorum", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Total number of requests ordered and output to the read path.", + "description": "The size of a strong quorum, needed for ordering consensus", "fieldConfig": { "defaults": { "color": { @@ -659,13 +652,13 @@ "overrides": [] }, "gridPos": { - "h": 12, + "h": 3, "w": 8, "x": 16, - "y": 12 + "y": 4 }, "hideTimeOverride": false, - "id": 40, + "id": 101, "maxDataPoints": 100, "options": { "colorMode": "value", @@ -687,25 +680,24 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "sum by (namespace,job,node) (histogram_sum(daml_sequencer_bftordering_output_block_size_requests{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}))", + "expr": "daml_sequencer_bftordering_topology_strong_quorum{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Total Output Requests", + "title": "Strong quorum", "type": "stat" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "description": "Sequencers that are part of the BFT ordering topology", "fieldConfig": { @@ -738,7 +730,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -771,10 +763,10 @@ ] }, "gridPos": { - "h": 21, - "w": 12, + "h": 7, + "w": 6, "x": 0, - "y": 24 + "y": 7 }, "id": 91, "options": { @@ -793,12 +785,11 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_topology_topology_member{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "interval": "", - "legendFormat": " (reporter: {{namespace}}.{{node}}) {{sequencer_id}}", + "expr": "daml_sequencer_bftordering_topology_topology_member{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "{{sequencer_id}}", "range": true, "refId": "A" } @@ -809,7 +800,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "description": "Sequencers that are allowed to propose blocks for ordering", "fieldConfig": { @@ -841,7 +832,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -878,10 +869,10 @@ ] }, "gridPos": { - "h": 21, - "w": 12, - "x": 12, - "y": 24 + "h": 7, + "w": 6, + "x": 6, + "y": 7 }, "id": 95, "options": { @@ -900,11 +891,11 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_topology_topology_leader{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": " (reporter: {{namespace}}.{{node}}) {{sequencer_id}}", + "expr": "daml_sequencer_bftordering_topology_topology_leader{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "{{sequencer_id}}", "range": true, "refId": "A" } @@ -915,7 +906,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "description": "", "fieldConfig": { @@ -947,7 +938,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -968,13 +959,26 @@ ] } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "hidden" + } + ] + } + ] }, "gridPos": { - "h": 21, - "w": 8, - "x": 0, - "y": 45 + "h": 7, + "w": 4, + "x": 12, + "y": 7 }, "id": 96, "options": { @@ -993,14 +997,11 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "editorMode": "code", - "exemplar": false, - "expr": "daml_sequencer_bftordering_p2p_authenticated_endpoint{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "format": "time_series", - "instant": false, - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{endpoint}}", + "expr": "daml_sequencer_bftordering_p2p_authenticated_endpoint{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "{{endpoint}}", "range": true, "refId": "A" } @@ -1011,7 +1012,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "description": "", "fieldConfig": { @@ -1080,10 +1081,10 @@ ] }, "gridPos": { - "h": 21, - "w": 8, - "x": 8, - "y": 45 + "h": 7, + "w": 4, + "x": 16, + "y": 7 }, "id": 97, "options": { @@ -1102,11 +1103,11 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_p2p_unauthenticated_endpoint{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{endpoint}}", + "expr": "daml_sequencer_bftordering_p2p_unauthenticated_endpoint{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "{{endpoint}}", "range": true, "refId": "A" } @@ -1117,7 +1118,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "description": "", "fieldConfig": { @@ -1186,10 +1187,10 @@ ] }, "gridPos": { - "h": 21, - "w": 8, - "x": 16, - "y": 45 + "h": 7, + "w": 4, + "x": 20, + "y": 7 }, "id": 98, "options": { @@ -1208,11 +1209,11 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_p2p_disconnected_endpoint{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{endpoint}}", + "expr": "daml_sequencer_bftordering_p2p_disconnected_endpoint{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "{{endpoint}}", "range": true, "refId": "A" } @@ -1282,7 +1283,7 @@ "h": 7, "w": 12, "x": 0, - "y": 66 + "y": 14 }, "id": 87, "options": { @@ -1290,10 +1291,10 @@ "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1304,8 +1305,8 @@ "uid": "prometheus" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by (namespace, job, node) (rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", - "legendFormat": "{{namespace}}.{{node}}", + "expr": "histogram_quantile($percentile, sum(rate(daml_sequencer_bftordering_global_requests_ordering_latency_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", + "legendFormat": "__auto", "range": true, "refId": "A" } @@ -1316,9 +1317,9 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Rate of requests ordered and output to the read path.", + "description": "Rate of blocks ordered and output to the read path.", "fieldConfig": { "defaults": { "color": { @@ -1355,75 +1356,41 @@ } }, "decimals": 2, - "links": [], "mappings": [], - "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", - "value": 0 + "value": null } ] }, "unit": "cps" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Height for last 3 hours" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "#447ebc", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Total Transactions for last 3 hours" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "#ef843c", - "mode": "fixed" - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 66 + "y": 14 }, - "hideTimeOverride": true, - "id": 88, + "hideTimeOverride": false, + "id": 68, "options": { "legend": { "calcs": [ - "mean", + "min", "max", - "sum" + "mean" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { - "hideZeros": false, - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -1431,25 +1398,44 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, + "disableTextWrap": false, "editorMode": "code", - "expr": "sum by (namespace, job, node) (irate(daml_sequencer_bftordering_global_ordered_requests_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))", + "exemplar": false, + "expr": "sum(irate(daml_sequencer_bftordering_global_ordered_blocks_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval]))", "format": "time_series", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", - "refId": "A" + "legendFormat": "All blocks -", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "$DS" + }, + "editorMode": "code", + "expr": "irate(daml_sequencer_bftordering_global_ordered_blocks_total{reporting_sequencer=\"$reporting_sequencer\", is_block_empty=\"true\"}[$__rate_interval])", + "hide": false, + "legendFormat": "Empty blocks -", + "range": true, + "refId": "B" } ], - "title": "Throughput - requests/s", + "title": "Throughput - blocks/s", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Number of requests per consensus block.", "fieldConfig": { @@ -1496,7 +1482,7 @@ "steps": [ { "color": "green", - "value": 0 + "value": null } ] }, @@ -1539,9 +1525,9 @@ "h": 7, "w": 12, "x": 0, - "y": 73 + "y": 21 }, - "hideTimeOverride": true, + "hideTimeOverride": false, "id": 49, "options": { "legend": { @@ -1555,24 +1541,23 @@ "showLegend": true }, "tooltip": { - "hideZeros": false, "mode": "multi", "sort": "none" } }, - "pluginVersion": "10.4.0", + "pluginVersion": "10.1.5", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "histogram_sum(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_requests{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))) / histogram_count(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_requests{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "expr": "histogram_sum(sum by(mode) (rate(daml_sequencer_bftordering_output_block_size_requests{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval]))) / histogram_count(sum by(mode) (rate(daml_sequencer_bftordering_output_block_size_requests{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "{{mode}} - Requests", "refId": "A" } ], @@ -1582,9 +1567,9 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Rate of blocks ordered and output to the read path.", + "description": "Rate of requests ordered and output to the read path.", "fieldConfig": { "defaults": { "color": { @@ -1621,75 +1606,100 @@ } }, "decimals": 2, + "links": [], "mappings": [], + "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", - "value": 0 + "value": null } ] }, "unit": "cps" }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Height for last 3 hours" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#447ebc", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Transactions for last 3 hours" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#ef843c", + "mode": "fixed" + } + } + ] + } + ] }, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 73 + "y": 21 }, - "hideTimeOverride": true, - "id": 68, + "hideTimeOverride": false, + "id": 88, "options": { "legend": { "calcs": [ - "min", + "mean", "max", - "mean" + "sum" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { - "hideZeros": false, "mode": "multi", "sort": "none" } }, - "pluginVersion": "10.4.0", + "pluginVersion": "10.1.5", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, - "disableTextWrap": false, "editorMode": "code", - "exemplar": false, - "expr": "sum by (namespace, job, node, is_block_empty) (irate(daml_sequencer_bftordering_global_ordered_blocks_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))", + "expr": "sum(irate(daml_sequencer_bftordering_global_ordered_requests_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval]))", "format": "time_series", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}} (empty: {{is_block_empty}})", - "range": true, - "refId": "A", - "useBackend": false + "legendFormat": "Requests/s -", + "refId": "A" } ], - "title": "Throughput - blocks/s", + "title": "Throughput - requests/s", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Number of batches (proofs of availability) per consensus block. Note that < 1 average batch per block likely means empty blocks are being created.", "fieldConfig": { @@ -1736,7 +1746,7 @@ "steps": [ { "color": "green", - "value": 0 + "value": null } ] }, @@ -1779,9 +1789,9 @@ "h": 7, "w": 12, "x": 0, - "y": 80 + "y": 28 }, - "hideTimeOverride": true, + "hideTimeOverride": false, "id": 89, "options": { "legend": { @@ -1795,24 +1805,23 @@ "showLegend": true }, "tooltip": { - "hideZeros": false, "mode": "multi", "sort": "none" } }, - "pluginVersion": "10.4.0", + "pluginVersion": "10.1.5", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "histogram_sum(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_batches{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval]))) / histogram_count(sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_batches{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "expr": "histogram_sum(sum by(mode) (rate(daml_sequencer_bftordering_output_block_size_batches{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval]))) / histogram_count(sum by(mode) (rate(daml_sequencer_bftordering_output_block_size_batches{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "{{mode}} - Requests", "refId": "A" } ], @@ -1822,7 +1831,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Rate of batches (proofs of availability) ordered and output to the read path.", "fieldConfig": { @@ -1869,7 +1878,7 @@ "steps": [ { "color": "green", - "value": 0 + "value": null } ] }, @@ -1912,9 +1921,9 @@ "h": 7, "w": 12, "x": 12, - "y": 80 + "y": 28 }, - "hideTimeOverride": true, + "hideTimeOverride": false, "id": 90, "options": { "legend": { @@ -1928,24 +1937,23 @@ "showLegend": true }, "tooltip": { - "hideZeros": false, "mode": "multi", "sort": "none" } }, - "pluginVersion": "10.4.0", + "pluginVersion": "10.1.5", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "irate(daml_sequencer_bftordering_global_ordered_batches_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])", + "expr": "irate(daml_sequencer_bftordering_global_ordered_batches_total{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "Batches/s", "refId": "A" } ], @@ -1955,223 +1963,7 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" - }, - "description": "Delay introduced by the sequencer core being too slow consuming blocks from ordering, in milliseconds", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 87 - }, - "hideTimeOverride": true, - "id": 102, - "options": { - "legend": { - "calcs": [ - "max" - ], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "10.4.0", - "targets": [ - { - "datasource": { - "uid": "prometheus" - }, - "disableTextWrap": false, - "editorMode": "code", - "exemplar": false, - "expr": "daml_sequencer_bftordering_sequencer_core_backpressure_current_delay_millis{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "format": "time_series", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "interval": "", - "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Sequencer core backpressure delay (ms)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "Size of the buffer in the subscription from the orderer to the sequencer core, in blocks", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 87 - }, - "hideTimeOverride": true, - "id": 103, - "options": { - "legend": { - "calcs": [ - "max", - "min", - "mean" - ], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "10.4.0", - "targets": [ - { - "datasource": { - "uid": "prometheus" - }, - "disableTextWrap": false, - "editorMode": "code", - "exemplar": false, - "expr": "daml_sequencer_bftordering_sequencer_core_subscription_buffer_size{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}\n", - "format": "time_series", - "fullMetaSearch": false, - "hide": false, - "includeNullMetadata": true, - "instant": false, - "interval": "", - "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}}", - "range": true, - "refId": "A", - "useBackend": false - } - ], - "title": "Sequencer core buffer size (blocks #)", - "type": "timeseries" - }, - - - - - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "description": "How many epochs a sequencer is blacklisted", "fieldConfig": { @@ -2214,8 +2006,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2230,7 +2021,7 @@ "h": 7, "w": 12, "x": 0, - "y": 94 + "y": 35 }, "id": 86, "options": { @@ -2241,7 +2032,7 @@ "showLegend": true }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2249,11 +2040,11 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "${DS}" }, "editorMode": "builder", - "expr": "daml_sequencer_bftordering_blacklist_sequencer{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{blacklist_sequencer}}", + "expr": "daml_sequencer_bftordering_blacklist_sequencer{reporting_sequencer=\"$reporting_sequencer\"}", + "legendFormat": "{{blacklist_sequencer}}", "range": true, "refId": "A" } @@ -2264,9 +2055,9 @@ { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Size of the stream buffer at various stages", + "description": "Wall-clock time of the ordered block being provided to the sequencer minus BFT time of the block", "fieldConfig": { "defaults": { "color": { @@ -2302,28 +2093,42 @@ "mode": "off" } }, - "decimals": 0, - "mappings": [], + "decimals": 2, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], "thresholds": { "mode": "absolute", "steps": [ { - "color": "green", - "value": 0 + "color": "green" + }, + { + "color": "red", + "value": 2 } ] }, - "unit": "none" + "unit": "s" }, "overrides": [] }, "gridPos": { "h": 7, - "w": 12, + "w": 6, "x": 12, - "y": 94 + "y": 35 }, - "id": 104, + "hideTimeOverride": true, + "id": 85, "options": { "legend": { "calcs": [], @@ -2332,8 +2137,7 @@ "showLegend": true }, "tooltip": { - "hideZeros": false, - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2341,33 +2145,29 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, - "disableTextWrap": false, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_block_stream_buffer_size{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "histogram_quantile($percentile, sum by(mode) (rate(daml_sequencer_bftordering_output_block_delay_duration_seconds{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", "format": "time_series", - "fullMetaSearch": false, - "includeNullMetadata": true, "instant": false, - "interval": "", + "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}.{{element}}", + "legendFormat": "{{mode}} - Block Delay", "range": true, - "refId": "A", - "useBackend": false + "refId": "A" } ], - "title": "Stream Buffer Metrics", + "title": "Block delay", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Wall-clock time of the ordered block being provided to the sequencer minus BFT time of the block", + "description": "Size of the payloads of blocks ordered and output to the read path.", "fieldConfig": { "defaults": { "color": { @@ -2404,45 +2204,70 @@ } }, "decimals": 2, - "mappings": [ - { - "options": { - "match": "null", - "result": { - "text": "N/A" - } - }, - "type": "special" - } - ], + "links": [], + "mappings": [], + "min": 0, "thresholds": { "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", - "value": 2 + "value": 80 } ] }, - "unit": "s" + "unit": "bytes" }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Height for last 3 hours" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#447ebc", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Transactions for last 3 hours" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#ef843c", + "mode": "fixed" + } + } + ] + } + ] }, "gridPos": { "h": 7, "w": 6, - "x": 12, - "y": 94 + "x": 18, + "y": 35 }, - "hideTimeOverride": true, - "id": 85, + "hideTimeOverride": false, + "id": 50, "options": { "legend": { - "calcs": [], + "calcs": [ + "mean", + "max" + ], "displayMode": "list", "placement": "bottom", "showLegend": true @@ -2452,33 +2277,31 @@ "sort": "none" } }, - "pluginVersion": "10.4.0", + "pluginVersion": "10.1.5", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "exemplar": false, - "expr": "histogram_quantile($percentile, sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_delay_duration_seconds{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "expr": "histogram_quantile($percentile, sum by(mode) (rate(daml_sequencer_bftordering_output_block_size_bytes{reporting_sequencer=\"$reporting_sequencer\"}[$__rate_interval])))", "format": "time_series", "instant": false, - "interval": "30s", + "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", - "range": true, + "legendFormat": "{{ mode }} - Block Size", "refId": "A" } ], - "title": "Block delay", + "title": "Output Block Payload Size", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Size of the payloads of blocks ordered and output to the read path.", + "description": "Count of non-compliant and possibly byzantine protocol behaviors detected by the currently selected sequencer.", "fieldConfig": { "defaults": { "color": { @@ -2514,7 +2337,7 @@ "mode": "off" } }, - "decimals": 2, + "decimals": 0, "links": [], "mappings": [], "min": 0, @@ -2522,16 +2345,11 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 + "color": "green" } ] }, - "unit": "bytes" + "unit": "none" }, "overrides": [ { @@ -2568,12 +2386,12 @@ }, "gridPos": { "h": 7, - "w": 6, - "x": 18, - "y": 94 + "w": 12, + "x": 0, + "y": 42 }, "hideTimeOverride": false, - "id": 50, + "id": 74, "options": { "legend": { "calcs": [ @@ -2582,7 +2400,7 @@ ], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "multi", @@ -2593,25 +2411,25 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "histogram_quantile($percentile, sum by(namespace, job, node, mode) (rate(daml_sequencer_bftordering_output_block_size_bytes{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}[$__rate_interval])))", + "expr": "sum(irate(daml_sequencer_bftordering_security_noncompliant_behavior_total[$__rate_interval]))", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}.{{mode}}", + "legendFormat": "__auto", "refId": "A" } ], - "title": "Output Block Payload Size", + "title": "Non-compliant behaviors", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "description": "Percentage of votes received by the currently selected sequencer at every major PBFT stage. Should be > 2/3 at all times, else a leader change (\"view change\" in PBFT parlance) occurs automatically.", "fieldConfig": { @@ -2633,8 +2451,7 @@ "mode": "percentage", "steps": [ { - "color": "red", - "value": null + "color": "red" }, { "color": "#EAB839", @@ -2655,10 +2472,10 @@ "overrides": [] }, "gridPos": { - "h": 28, - "w": 24, - "x": 0, - "y": 101 + "h": 7, + "w": 12, + "x": 12, + "y": 42 }, "hideTimeOverride": false, "id": 73, @@ -2681,29 +2498,29 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_consensus_prepare_votes_percent{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_consensus_prepare_votes_percent{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, - "legendFormat": "(reporter: {{namespace}}.{{node}}) Prepare {{voting_sequencer}}", + "legendFormat": "Prepare {{voting_sequencer}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "daml_sequencer_bftordering_consensus_commit_votes_percent{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_consensus_commit_votes_percent{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "hide": false, - "legendFormat": "(reporter: {{namespace}}.{{node}}) Commit {{voting_sequencer}}", + "legendFormat": "Commit {{voting_sequencer}}", "range": true, "refId": "B" } @@ -2728,142 +2545,39 @@ "type": "state-timeline" }, { + "collapsed": false, "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Count of non-compliant and possibly byzantine protocol behaviors detected by the currently selected sequencer.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "decimals": 0, - "links": [], - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "none" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Height for last 3 hours" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "#447ebc", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Total Transactions for last 3 hours" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "#ef843c", - "mode": "fixed" - } - } - ] - } - ] - }, "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 129 - }, - "hideTimeOverride": false, - "id": 74, - "options": { - "legend": { - "calcs": [ - "mean", - "max" - ], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } + "h": 1, + "w": 24, + "x": 0, + "y": 49 }, - "pluginVersion": "10.1.5", + "id": 55, + "panels": [], + "repeat": "instance", + "repeatDirection": "h", "targets": [ { "datasource": { + "type": "prometheus", "uid": "prometheus" }, - "editorMode": "code", - "expr": "sum by (namespace, job, node) (irate(daml_sequencer_bftordering_security_noncompliant_behavior_total{namespace=~\"$namespace\", job=~\"$job\", node=~\"$node\"}[$__rate_interval]))", - "format": "time_series", - "instant": false, - "interval": "", - "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "refId": "A" } ], - "title": "Non-compliant behaviors", - "type": "timeseries" + "title": "Sequencer overview: $reporting_sequencer", + "type": "row" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Size of the incoming requests buffer (\"mempool\") for the selected sequencer. Its maximum size in bytes and requests count can be currently set via static configuration.", + "description": "Number of proposals requested by consensus.", "fieldConfig": { "defaults": { "color": { @@ -2905,8 +2619,7 @@ "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null + "color": "#7eb26d" }, { "color": "#ef843c", @@ -2925,20 +2638,20 @@ "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 129 + "x": 0, + "y": 50 }, "hideTimeOverride": true, - "id": 78, + "id": 77, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -2946,29 +2659,28 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_ingress_requests_queued{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_availability_requested_proposals{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Mempool Size (requests)", + "title": "Block proposals requested by consensus", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Number of proposals requested by consensus.", + "description": "Number of batches requested by the availability module and not yet provided.", "fieldConfig": { "defaults": { "color": { @@ -3010,8 +2722,7 @@ "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null + "color": "#7eb26d" }, { "color": "#ef843c", @@ -3030,20 +2741,20 @@ "gridPos": { "h": 7, "w": 12, - "x": 0, - "y": 136 + "x": 12, + "y": 50 }, "hideTimeOverride": true, - "id": 77, + "id": 76, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3051,29 +2762,28 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_availability_requested_proposals{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_mempool_requested_batches{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Block proposals requested by consensus", + "title": "Outstanding batch requests", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Number of batches requested to be proposed in consensus.", + "description": "Number of batches that are being disseminated.", "fieldConfig": { "defaults": { "color": { @@ -3115,8 +2825,7 @@ "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null + "color": "#7eb26d" }, { "color": "#ef843c", @@ -3135,20 +2844,20 @@ "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 136 + "x": 0, + "y": 57 }, "hideTimeOverride": true, - "id": 79, + "id": 82, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3156,29 +2865,28 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_availability_requested_batches{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_availability_disseminating_batches{reporting_sequencer=\"$reporting_sequencer\", ready_for_consensus=~\"false|true\"}", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Block proposals requested by consensus (batches)", + "title": "Batches being disseminated", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Number of batches that are being disseminated.", + "description": "Size of the incoming requests buffer (\"mempool\") for the selected sequencer. Its maximum size in bytes and requests count can be currently set via static configuration.", "fieldConfig": { "defaults": { "color": { @@ -3220,8 +2928,7 @@ "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null + "color": "#7eb26d" }, { "color": "#ef843c", @@ -3240,20 +2947,20 @@ "gridPos": { "h": 7, "w": 12, - "x": 0, - "y": 143 + "x": 12, + "y": 57 }, "hideTimeOverride": true, - "id": 82, + "id": 78, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3261,29 +2968,28 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_availability_disseminating_batches{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ready_for_consensus=~\"false|true\"}", + "expr": "daml_sequencer_bftordering_ingress_requests_queued{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Batches being disseminated", + "title": "Mempool Size (requests)", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Number of batches requested by the availability module and not yet provided.", + "description": "Number of requests in batches that are being disseminated.", "fieldConfig": { "defaults": { "color": { @@ -3325,8 +3031,7 @@ "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null + "color": "#7eb26d" }, { "color": "#ef843c", @@ -3345,20 +3050,20 @@ "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 143 + "x": 0, + "y": 64 }, "hideTimeOverride": true, - "id": 76, + "id": 83, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3366,57 +3071,28 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_mempool_requested_batches{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"}", + "expr": "daml_sequencer_bftordering_availability_disseminating_requests{reporting_sequencer=\"$reporting_sequencer\", ready_for_consensus=~\"false|true\"}", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Outstanding batch requests", + "title": "Requests being disseminated", "type": "timeseries" }, - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 150 - }, - "id": 55, - "panels": [], - "repeat": "instance", - "repeatDirection": "h", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "refId": "A" - } - ], - "title": "Sequencer overview: $reporting_sequencer", - "type": "row" - }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Number of requests in batches that are being disseminated.", + "description": "Number of batches requested to be proposed in consensus.", "fieldConfig": { "defaults": { "color": { @@ -3458,8 +3134,7 @@ "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null + "color": "#7eb26d" }, { "color": "#ef843c", @@ -3478,20 +3153,20 @@ "gridPos": { "h": 7, "w": 12, - "x": 0, - "y": 151 + "x": 12, + "y": 64 }, "hideTimeOverride": true, - "id": 83, + "id": 79, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, @@ -3499,29 +3174,28 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", "exemplar": false, - "expr": "daml_sequencer_bftordering_availability_disseminating_requests{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ready_for_consensus=~\"false|true\"}", + "expr": "daml_sequencer_bftordering_availability_requested_batches{reporting_sequencer=\"$reporting_sequencer\"}", "format": "time_series", "instant": false, "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", "range": true, "refId": "A" } ], - "title": "Requests being disseminated", + "title": "Block proposals requested by consensus (batches)", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Total network ingress for the currently selected sequencer split by sender.", + "description": "Number of payload bytes in requests within batches that are being disseminated.", "fieldConfig": { "defaults": { "color": { @@ -3534,7 +3208,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 100, + "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, @@ -3547,28 +3221,31 @@ "scaleDistribution": { "type": "linear" }, - "showPoints": "never", + "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, - "links": [], + "decimals": 0, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "#7eb26d" }, { - "color": "red", - "value": 80 + "color": "#ef843c", + "value": 10 + }, + { + "color": "#e24d42", + "value": 20 } ] }, @@ -3577,48 +3254,51 @@ "overrides": [] }, "gridPos": { - "h": 14, - "w": 6, - "x": 12, - "y": 151 + "h": 7, + "w": 12, + "x": 0, + "y": 71 }, - "id": 59, + "hideTimeOverride": true, + "id": 84, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }, - "pluginVersion": "10.1.5", + "pluginVersion": "9.5.12", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "sum by(namespace, job, node, source_sequencer) (daml_sequencer_bftordering_p2p_receive_received_bytes_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"})", + "exemplar": false, + "expr": "daml_sequencer_bftordering_availability_disseminating_bytes{reporting_sequencer=\"$reporting_sequencer\", ready_for_consensus=~\"false|true\"}", "format": "time_series", + "instant": false, + "interval": "30s", "intervalFactor": 1, - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{source_sequencer}}", "range": true, "refId": "A" } ], - "title": "Total Network Input", + "title": "Bytes being disseminated", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Total network egress for the currently selected sequencer split by destination.", + "description": "Total network ingress for the currently selected sequencer split by sender.", "fieldConfig": { "defaults": { "color": { @@ -3660,8 +3340,11 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" + }, + { + "color": "red", + "value": 80 } ] }, @@ -3670,18 +3353,18 @@ "overrides": [] }, "gridPos": { - "h": 14, + "h": 7, "w": 6, - "x": 18, - "y": 151 + "x": 12, + "y": 71 }, - "id": 58, + "id": 59, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "single", @@ -3692,26 +3375,26 @@ "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "expr": "sum by(namespace, job, node, target_sequencer) (daml_sequencer_bftordering_p2p_send_sent_bytes_total{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\"})", + "expr": "sum by(source_sequencer) (daml_sequencer_bftordering_p2p_receive_received_bytes_total{reporting_sequencer=\"$reporting_sequencer\"})", "format": "time_series", "intervalFactor": 1, - "legendFormat": "(reporter: {{namespace}}.{{node}}) {{target_sequencer}}", + "legendFormat": "{{peer_id}}", "range": true, "refId": "A" } ], - "title": "Total Network Output", + "title": "Total Network Input", "type": "timeseries" }, { "datasource": { "type": "prometheus", - "uid": "prometheus" + "uid": "$DS" }, - "description": "Number of payload bytes in requests within batches that are being disseminated.", + "description": "Total network egress for the currently selected sequencer split by destination.", "fieldConfig": { "defaults": { "color": { @@ -3724,7 +3407,7 @@ "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", - "fillOpacity": 10, + "fillOpacity": 100, "gradientMode": "none", "hideFrom": { "legend": false, @@ -3737,32 +3420,23 @@ "scaleDistribution": { "type": "linear" }, - "showPoints": "auto", + "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", - "mode": "none" + "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, - "decimals": 0, + "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { - "color": "#7eb26d", - "value": null - }, - { - "color": "#ef843c", - "value": 10 - }, - { - "color": "#e24d42", - "value": 20 + "color": "green" } ] }, @@ -3772,43 +3446,39 @@ }, "gridPos": { "h": 7, - "w": 12, - "x": 0, - "y": 158 + "w": 6, + "x": 18, + "y": 71 }, - "hideTimeOverride": true, - "id": 84, + "id": 58, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { - "mode": "multi", + "mode": "single", "sort": "none" } }, - "pluginVersion": "9.5.12", + "pluginVersion": "10.1.5", "targets": [ { "datasource": { - "uid": "prometheus" + "uid": "$DS" }, "editorMode": "code", - "exemplar": false, - "expr": "daml_sequencer_bftordering_availability_disseminating_bytes{namespace=~\"$namespace\",job=~\"$job\",node=~\"$node\",ready_for_consensus=~\"false|true\"}", + "expr": "sum by(target_sequencer) (daml_sequencer_bftordering_p2p_send_sent_bytes_total{reporting_sequencer=\"$reporting_sequencer\"})", "format": "time_series", - "instant": false, - "interval": "30s", "intervalFactor": 1, - "legendFormat": "{{namespace}}.{{node}}", + "legendFormat": "{{peer_id}}", "range": true, "refId": "A" } ], - "title": "Bytes being disseminated", + "title": "Total Network Output", "type": "timeseries" } ], @@ -3821,102 +3491,49 @@ "templating": { "list": [ { - "allValue": ".*", - "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "definition": "label_values(daml_health_status,namespace}", - "hide": 0, - "includeAll": true, - "label": "Namespace", - "multi": true, - "name": "namespace", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(daml_health_status,namespace)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 2, - "regex": "", - "skipUrlSync": false, - "sort": 7, - "type": "query" - }, - { - "allValue": ".*", "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "prometheus", - "uid": "prometheus" + "selected": false, + "text": "default", + "value": "default" }, - "definition": "label_values(daml_health_status{namespace=~\"$namespace\"},job)", "hide": 0, - "includeAll": true, - "label": "Job", - "multi": true, - "name": "job", + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "DS", "options": [], - "query": { - "qryType": 1, - "query": "label_values(daml_health_status{namespace=~\"$namespace\"},job)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 2, + "query": "prometheus", + "queryValue": "", + "refresh": 1, "regex": "", "skipUrlSync": false, - "sort": 7, - "type": "query" + "type": "datasource" }, { - "allValue": ".*", "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] + "selected": false, + "text": "SEQ::sequencer1::12201e092c1890e837b24f3d2656885c43e96ffe432ed8a4c85ea1c02e3f52c7b353", + "value": "SEQ::sequencer1::12201e092c1890e837b24f3d2656885c43e96ffe432ed8a4c85ea1c02e3f52c7b353" }, "datasource": { "type": "prometheus", "uid": "prometheus" }, - "definition": "label_values(daml_health_status{namespace=~\"$namespace\", job=~\"$job\"},node)", + "definition": "label_values(reporting_sequencer)", "hide": 0, - "includeAll": true, - "label": "Node", - "multi": true, - "name": "node", + "includeAll": false, + "label": "Reporting sequencer", + "multi": false, + "name": "reporting_sequencer", "options": [], "query": { - "qryType": 1, - "query": "label_values(daml_health_status{namespace=~\"$namespace\", job=~\"$job\"},node)", + "query": "label_values(reporting_sequencer)", "refId": "PrometheusVariableQueryEditor-VariableQuery" }, - "refresh": 2, + "refresh": 1, "regex": "", "skipUrlSync": false, - "sort": 7, + "sort": 0, "type": "query" }, { @@ -4016,6 +3633,6 @@ "timezone": "", "title": "BFT ordering", "uid": "UJyurCTWz", - "version": 2, + "version": 1, "weekStart": "" } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala index 7d612d7722..a077b51a72 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/admin/api/client/GrpcCtlRunnerTest.scala @@ -27,7 +27,6 @@ class GrpcCtlRunnerTest extends AsyncWordSpec with BaseTest { channel, None, 1000.milliseconds, - _ => false, ) map { result => result shouldBe "result" } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala index ce6b4cf486..5bdeb9d3af 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala @@ -71,7 +71,7 @@ final class ConsoleTest extends AnyWordSpec with BaseTest { ) private abstract class TestEnvironment(val config: CantonConfig = DefaultConfig) { - val environment: CantonEnvironment = mock[CantonEnvironment] + val environment: Environment = mock[Environment] val participants: ParticipantNodes[ParticipantNodeBootstrap, ParticipantNode] = mock[ParticipantNodes[ParticipantNodeBootstrap, ParticipantNode]] val sequencers: SequencerNodes = mock[SequencerNodes] @@ -126,7 +126,7 @@ final class ConsoleTest extends AnyWordSpec with BaseTest { .thenReturn(GenericCommandError("Mocked error")) private val consoleEnvironment = - new CantonConsoleEnvironment( + new ConsoleEnvironment( environment, consoleOutput = testConsoleOutput, ) { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala index 55051ae349..a0b61472f4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala @@ -10,7 +10,7 @@ import com.digitalasset.canton.console.BufferedProcessLogger import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.environment.Environment import com.digitalasset.canton.integration.{ - CantonBaseIntegrationTest, + BaseIntegrationTest, ConfigTransform, EnvironmentDefinition, IsolatedEnvironments, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala index 84c83ea9b7..f0b792fc7c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala @@ -14,7 +14,7 @@ import com.digitalasset.canton.integration.bootstrap.{ } import com.digitalasset.canton.integration.util.MultiSynchronizerFeatureFlag import com.digitalasset.canton.integration.{ - CantonBaseIntegrationTest, + BaseIntegrationTest, CommunityIntegrationTest, ConfigTransforms, EnvironmentDefinition, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/jsonapi/HttpServiceUserFixture.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/jsonapi/HttpServiceUserFixture.scala index 624d45253c..dbd146a1d6 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/jsonapi/HttpServiceUserFixture.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/jsonapi/HttpServiceUserFixture.scala @@ -43,7 +43,7 @@ trait HttpServiceUserFixture extends PekkoBeforeAndAfterAll { this: Suite with C } protected def extractHeaders(w3cContext: W3CTraceContext): Seq[HttpHeader] = - w3cContext.asHeaders.toSeq.map(h => HttpHeader.parse(h._1.value, h._2)).collect { + w3cContext.asHeaders.toSeq.map(h => HttpHeader.parse(h._1, h._2)).collect { case ParsingResult.Ok(header, _) => header } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala index 4a96d7348d..37fff2258d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala @@ -13,8 +13,7 @@ import com.digitalasset.canton.version.{HashingSchemeVersion, ProtocolVersion} import java.util.UUID -final class InteractiveSubmissionVersioningIntegrationTest - extends InteractiveSubmissionNuckSetupTest { +class InteractiveSubmissionVersioningIntegrationTest extends InteractiveSubmissionNuckSetupTest { "Interactive submission" should { val supportedHashingSchemeVersions: Set[HashingSchemeVersion] = diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/BasePerformanceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/BasePerformanceIntegrationTest.scala index d88a9e716a..ab0d3a349a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/BasePerformanceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/BasePerformanceIntegrationTest.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.performance.PartyRole.{ DvpTrader, Master, MasterDynamicConfig, + Transfer, } import com.digitalasset.canton.performance.RateSettings.SubmissionRateSettings import com.digitalasset.canton.performance.elements.dvp.TraderDriver @@ -171,6 +172,7 @@ trait BasePerformanceIntegrationTestCommon case role: ActivePartyRole => role match { case x: DvpTrader => x.focus(_.settings).replace(whackMe) + case x: Transfer => x.focus(_.settings).replace(whackMe) case x: DvpIssuer => x } case m => m diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MemberAuthLoadWithSequencerRestartIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MemberAuthLoadWithSequencerRestartIntegrationTest.scala index e2bb08a49a..d7c5b6cee1 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MemberAuthLoadWithSequencerRestartIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MemberAuthLoadWithSequencerRestartIntegrationTest.scala @@ -4,15 +4,18 @@ package com.digitalasset.canton.integration.tests.performance import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.digitalasset.canton.annotations.UnstableTest import com.digitalasset.canton.concurrent.Threading import com.digitalasset.canton.config import com.digitalasset.canton.config.NonNegativeDuration import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.integration.plugins.UsePostgres import com.digitalasset.canton.integration.{ConfigTransforms, EnvironmentDefinition} +import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.performance.RateSettings.SubmissionRateSettings import com.digitalasset.canton.performance.elements.DriverStatus import com.digitalasset.canton.performance.{PerformanceRunner, RateSettings} +import com.digitalasset.canton.util.Thereafter.syntax.ThereafterOps import monocle.macros.syntax.lens.* import scala.concurrent.Future @@ -48,8 +51,6 @@ abstract class MemberAuthLoadWithSequencerRestartIntegrationTest .replace(config.NonNegativeFiniteDuration.ofMillis(0)) .focus(_.maxConnectionRetryDelay) .replace(config.NonNegativeFiniteDuration.ofMillis(250)) - .focus(_.useNewConnectionPool) // works for both - .replace(true) .focus(_.enableAmplificationImprovements) // should be true anyway .replace(true) ), @@ -109,6 +110,7 @@ abstract class MemberAuthLoadWithSequencerRestartIntegrationTest stats.sizeIs >= 4 && stats.forall { case c: DriverStatus.TraderStatus => c.mode == "THROUGHPUT" + case _: DriverStatus.TransferStatus => true case _: DriverStatus.MasterStatus => true } } @@ -122,8 +124,16 @@ abstract class MemberAuthLoadWithSequencerRestartIntegrationTest sequencer.start() Threading.sleep(4000) } - val cf = Future.sequence(Seq(runnerP1, runnerP2).map(_.closeF())) - cf.futureValue + val cf = + FutureUnlessShutdown.outcomeF(Future.sequence(Seq(runnerP1, runnerP2).map(_.closeF()))) + cf.thereafter { _ => + logger.info("Performance runners stopped") + // Ensure that the participants are stopped before the end of the test, otherwise they may + // still log warnings because of timeouts triggering after the test case is over + participant1.stop() + participant2.stop() + logger.info("Participants stopped") + }.futureValueUS }, forEvery(_)( acceptableLogMessageExt( @@ -132,15 +142,17 @@ abstract class MemberAuthLoadWithSequencerRestartIntegrationTest "Is the server running? Did you configure the server", // some topology transactions must be retried "failed the following topology transaction", + // can happen if a connection in the pool is in a stopped state, which can occur during sequencer restarts + "Connection is not started", ), Seq(), ) ), ) } - } +@UnstableTest class MemberAuthLoadWithSequencerRestartIntegrationTestPostgres extends MemberAuthLoadWithSequencerRestartIntegrationTest { setupPlugins(new UsePostgres(loggerFactory)) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MultiSynchronizerPerformanceRunnerIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MultiSynchronizerPerformanceRunnerIntegrationTest.scala index d52e32ea67..a6bb5d8e57 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MultiSynchronizerPerformanceRunnerIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/MultiSynchronizerPerformanceRunnerIntegrationTest.scala @@ -52,6 +52,7 @@ class MultiSynchronizerPerformanceRunnerIntegrationTest .ofMillis(sequencerClientAcknowledgementIntervalMs.toLong) .toInternal ), + ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/SequencerAggregationPerformanceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/SequencerAggregationPerformanceIntegrationTest.scala index 82eb5b8b86..e8f1eb927d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/SequencerAggregationPerformanceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/performance/SequencerAggregationPerformanceIntegrationTest.scala @@ -12,22 +12,24 @@ import com.digitalasset.canton.admin.api.client.data.{ SubmissionRequestAmplification, SynchronizerConnectionConfig, } +import com.digitalasset.canton.concurrent.Threading import com.digitalasset.canton.config.NonNegativeDuration import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.{LocalParticipantReference, ParticipantReference} -import com.digitalasset.canton.integration.EnvironmentDefinition import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, } import com.digitalasset.canton.integration.plugins.UsePostgres import com.digitalasset.canton.integration.tests.performance.BasePerformanceIntegrationTest.toConnectivity +import com.digitalasset.canton.integration.{EnvironmentDefinition, TestConsoleEnvironment} import com.digitalasset.canton.performance.PartyRole.{ DvpIssuer, DvpTrader, Master, MasterDynamicConfig, } +import com.digitalasset.canton.performance.elements.DriverStatus import com.digitalasset.canton.performance.elements.dvp.TraderDriver import com.digitalasset.canton.performance.model.java.orchestration.runtype import com.digitalasset.canton.performance.{ @@ -43,12 +45,18 @@ import monocle.macros.syntax.lens.* import scala.concurrent.Future import scala.concurrent.duration.* +/** Put sequencer aggregation under stress + * + * This test originally started to test sequencer aggregation performance with 5 mediators and + * threshold 5 + */ class SequencerAggregationPerformanceIntegrationTest extends BasePerformanceIntegrationTest { setupPlugins(new UsePostgres(loggerFactory)) private val masterName = "SAMaster" - private val issuersPerNode: Int = 2 - private val tradersPerNode: Int = 4 + private val issuersPerNode: Int = 1 + private val tradersPerNode: Int = 2 + private val racyTopologyStateCrashRecovery = true private def runnerConfigs( activeNodes: Seq[LocalParticipantReference] @@ -59,7 +67,7 @@ class SequencerAggregationPerformanceIntegrationTest extends BasePerformanceInte totalCycles = 100, // Change this to increase the number of test iterations reportFrequency = 2, runType = new runtype.DvpRun( - 1000L, // change this to increase the number of assets per issuer, + 500L, // change this to increase the number of assets per issuer, 0, 0, TraderDriver.toPartyGrowth(0), @@ -112,7 +120,7 @@ class SequencerAggregationPerformanceIntegrationTest extends BasePerformanceInte daName, sequencers = env.sequencers.local, mediators = env.mediators.all, - synchronizerOwners = Seq(sequencer1, sequencer2), + synchronizerOwners = Seq(sequencer1, sequencer2, sequencer3), synchronizerThreshold = PositiveInt.two, mediatorThreshold = env.mediators.all.size, ) @@ -181,6 +189,24 @@ class SequencerAggregationPerformanceIntegrationTest extends BasePerformanceInte } + private def adjustMediatorThreshold(threshold: PositiveInt, latencyAt3: Long = 2000)(implicit + env: TestConsoleEnvironment + ): Unit = { + import env.* + Seq(sequencer1, sequencer2, sequencer3).zipWithIndex.foreach { case (node, idx) => + logger.debug(s"Setting mediator thresholds on ${node.name} -> $threshold") + node.topology.mediators.propose_delta( + sequencer1.synchronizer_id, + NonNegativeInt.zero, + updateThreshold = Some(threshold), + synchronize = None, + ) + if (idx == 2) { + Threading.sleep(latencyAt3) + } + } + } + "start performance runners on p1 and p2" in { implicit env => import env.* @@ -195,12 +221,45 @@ class SequencerAggregationPerformanceIntegrationTest extends BasePerformanceInte env.environment.addUserCloseable(runner) runner } + val runnerF = Future.sequence(runners.map(r => r.startup())) + + // wait until the system really started up + eventually(timeUntilSuccess = 2.minutes) { + val mode = runners.headOption.value.status().collectFirst { + case DriverStatus.MasterStatus(_, mode, _, _, _, _) => mode + } + mode should contain("THROUGHPUT") + } loggerFactory.assertLoggedWarningsAndErrorsSeq( { + if (racyTopologyStateCrashRecovery) { + Seq(2, 1, 4, 1, 3).foreach { threshold => + logger.info("Testing with threshold " + threshold) + adjustMediatorThreshold(PositiveInt.tryCreate(threshold)) + val p1Runner = runners.headOption.value + p1Runner.setActive(false) + participant1.synchronizers.disconnect_all() + participant1.synchronizers.reconnect_all() + p1Runner.setActive(true) + eventually() { + Seq(sequencer1, sequencer2, sequencer3).foreach { seq => + val trs = seq.topology.mediators + .list(sequencer1.synchronizer_id, group = Some(NonNegativeInt.zero)) + .loneElement + logger.info( + s"Checking for ${seq.name} THRESHOLD=${trs.item.threshold} vs $threshold" + ) + trs.item.threshold.value shouldBe threshold + } + } + Threading.sleep(2000) // let it progress a bit + + } + } val res = waitTimeout .await("waiting for per runners to complete")( - Future.sequence(runners.map(r => r.startup())) + runnerF ) .sequence_ res shouldBe Right(()) @@ -213,7 +272,12 @@ class SequencerAggregationPerformanceIntegrationTest extends BasePerformanceInte // close runners Future.sequence(runners.map(_.closeF())).map(_ => ()).futureValue }, - forEvery(_)(acceptableLogMessage), + forEvery(_)( + acceptableLogMessageExt( + Seq("SUBMISSION_SYNCHRONIZER_NOT_READY", "No connection available"), + Seq(), + ) + ), ) } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/release/CliIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/release/CliIntegrationTest.scala index ea15b8b1c5..ef484e381f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/release/CliIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/release/CliIntegrationTest.scala @@ -36,8 +36,9 @@ class CliIntegrationTest extends ReleaseArtifactIntegrationTestUtils { s"$cantonBin --help" ! processLogger checkOutput( processLogger, - shouldContain = - Seq("Usage: canton [daemon|run|generate|sandbox|sandbox-console] [options] ..."), + shouldContain = Seq( + "Usage: canton [daemon|run|generate|sandbox|sandbox-interactive|sandbox-console] [options] ..." + ), ) } @@ -45,8 +46,9 @@ class CliIntegrationTest extends ReleaseArtifactIntegrationTestUtils { s"$cantonBin" ! processLogger checkOutput( processLogger, - shouldContain = - Seq("Usage: canton [daemon|run|generate|sandbox|sandbox-console] [options] ..."), + shouldContain = Seq( + "Usage: canton [daemon|run|generate|sandbox|sandbox-interactive|sandbox-console] [options] ..." + ), shouldSucceed = false, ) } @@ -285,5 +287,34 @@ class CliIntegrationTest extends ReleaseArtifactIntegrationTestUtils { ) } + "sandbox started in multi-sync mode is able to perform assignments" in { processLogger => + Process("rm -f log/canton.log", Some(new java.io.File(cantonDir))) ! + val scriptFile = new java.io.File(s"$resourceDir/scripts/multi-sync-smoke-test.canton") + val cmd = + s"bin/canton sandbox-interactive --multi-sync -C canton.features.enable-testing-commands=yes --no-tty" + Process(cmd, Some(new java.io.File(cantonDir))) #< scriptFile ! processLogger + val logLines = (File(cantonDir) / "log" / "canton.log").lines() + val expectedLine = "MULTI-SYNC: Smoke test success" + forAtLeast(1, logLines)(_ should endWith(expectedLine)) + checkOutput(processLogger) + } + + "sandbox started in nuck mode accepts packages containing keys" in { processLogger => + Process("rm -f log/canton.log", Some(new java.io.File(cantonDir))) ! + val scriptFile = new java.io.File(s"$resourceDir/scripts/nuck-smoke-test.canton") + val cmd = Seq( + "bin/canton", + "sandbox-interactive", + "--nuck", + "--no-tty", + s"-Dnuck-smoke-test.dar-path=$CantonTestsLF23Path", + "-Dnuck-smoke-test.package-name=CantonTestsLF23", + ) + Process(cmd, Some(new java.io.File(cantonDir))) #< scriptFile ! processLogger + val logLines = (File(cantonDir) / "log" / "canton.log").lines() + val expectedLine = "NUCK: Smoke test success" + forAtLeast(1, logLines)(_ should endWith(expectedLine)) + checkOutput(processLogger) + } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/RehydrationIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/RehydrationIntegrationTest.scala index e9e0d6da98..f9dc9c6de3 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/RehydrationIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/RehydrationIntegrationTest.scala @@ -145,7 +145,6 @@ abstract class RehydrationIntegrationTest import env.* participant1.dars.upload(CantonExamplesPath) - sv1Backend.start() val runF = (1 to iterations: Seq[Int]).parTraverse_ { i => Future { @@ -316,7 +315,7 @@ abstract class RehydrationIntegrationTest participant2.health.ping(participant2, timeout = 30.seconds) } }, - expectedLogs* + expectedLogs *, ) } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala index 309ef2387a..f57a58660a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala @@ -3,16 +3,6 @@ package com.digitalasset.canton.integration.tests.upgrade.lsu -import com.daml.nonempty.NonEmpty -import com.digitalasset.canton.SequencerAlias -import com.digitalasset.canton.admin.api.client.data.RegisteredSynchronizer.Status -import com.digitalasset.canton.admin.api.client.data.{ - KnownPhysicalSynchronizerId, - RegisteredSynchronizer, - SequencerConnection, - SynchronizerConnectionConfig, - SynchronizerPredecessor, -} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.* import com.digitalasset.canton.integration.* @@ -21,10 +11,7 @@ import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} import com.digitalasset.canton.integration.tests.examples.IouSyntax -import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.Fixture -import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuEndToEndIntegrationTest.ExpectedRegisteredSynchronizers import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer -import monocle.macros.syntax.lens.* import java.time.Duration import scala.jdk.CollectionConverters.* @@ -77,25 +64,6 @@ sealed trait LsuEndToEndIntegrationTest extends LsuBase { val fixture = fixtureWithDefaults() performSynchronizerNodesLsu(fixture) - val expectedRegisteredSynchronizers = clue("Values returned by list registered before LSU") { - val (currentConfig, currentConfiguredPsid, _) = - participant1.synchronizers.list_registered().loneElement - - val expectedRegisteredSynchronizers = - ExpectedRegisteredSynchronizers(currentConfig, fixture) - - currentConfiguredPsid.toOption.value shouldBe fixture.currentPsid - - eventually() { - participant1.synchronizers.list_all_registered() should contain theSameElementsAs Seq( - expectedRegisteredSynchronizers.currentRegisteredSynchronizerBeforeLsu, - expectedRegisteredSynchronizers.newRegisteredSynchronizerBeforeLsu, - ) - } - - expectedRegisteredSynchronizers - } - environment.simClock.foreach(_.advanceTo(upgradeTime.immediateSuccessor)) transferTraffic() @@ -105,22 +73,6 @@ sealed trait LsuEndToEndIntegrationTest extends LsuBase { participants.all.forall(_.synchronizers.is_connected(fixture.currentPsid)) shouldBe false } - clue("Values returned by list registered after LSU") { - val (_, currentConfiguredPsid, _) = - participant1.synchronizers.list_registered().loneElement - - currentConfiguredPsid.toOption.value shouldBe fixture.newPsid - - eventually() { - participant1.synchronizers.list_all_registered() should contain theSameElementsAs Seq( - expectedRegisteredSynchronizers.currentRegisteredSynchronizerBeforeLsu - .copy(isConnected = false, status = Status.LsuSource), - expectedRegisteredSynchronizers.newRegisteredSynchronizerBeforeLsu - .copy(isConnected = true, status = Status.Active), - ) - } - } - oldSynchronizerNodes.all.stop() waitForTargetTimeOnSequencer(sequencer2, upgradeTime.immediateSuccessor, logger) @@ -164,52 +116,6 @@ sealed trait LsuEndToEndIntegrationTest extends LsuBase { } } -private object LsuEndToEndIntegrationTest { - import org.scalatest.OptionValues.* - - final case class ExpectedRegisteredSynchronizers( - initialSynchronizerConnectionConfig: SynchronizerConnectionConfig, - fixture: Fixture, - )(implicit env: TestConsoleEnvironment) { - import env.* - - val currentRegisteredSynchronizerBeforeLsu = RegisteredSynchronizer( - config = initialSynchronizerConnectionConfig, - status = Status.Active, - psid = KnownPhysicalSynchronizerId(fixture.currentPsid), - predecessor = None, - isConnected = true, - ) - - val newSequencerConnections: NonEmpty[Map[SequencerAlias, SequencerConnection]] = NonEmpty - .from( - Map( - sequencer1.sequencerAlias -> sequencer2.sequencerConnection - .copy(sequencerAlias = sequencer1.sequencerAlias, sequencerId = Some(sequencer1.id)) - ) - ) - .value - - val newRegisteredSynchronizerBeforeLsu = RegisteredSynchronizer( - config = initialSynchronizerConnectionConfig - .focus(_.sequencerConnections.aliasToConnection) - .replace(newSequencerConnections) - .focus(_.synchronizerId) - .replace(Some(fixture.newPsid)), - status = Status.LsuTarget, - psid = KnownPhysicalSynchronizerId(fixture.newPsid), - predecessor = Some( - SynchronizerPredecessor( - psid = fixture.currentPsid, - upgradeTime = fixture.upgradeTime, - isLateUpgrade = false, - ) - ), - isConnected = false, - ) - } -} - final class LsuEndToEndSimClockIntegrationTest extends LsuEndToEndIntegrationTest final class LsuEndToEndWallClockIntegrationTest extends LsuEndToEndIntegrationTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala index 43ab712b3f..575a195c15 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala @@ -16,11 +16,7 @@ import com.digitalasset.canton.integration.util.PartyToParticipantDeclarativeCom PartyHostingState, Serial, } -import com.digitalasset.canton.integration.{ - CantonTestEnvironment, - PartyTopologyUtils, - TestEnvironment, -} +import com.digitalasset.canton.integration.{PartyTopologyUtils, TestEnvironment} import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.transaction.{ HostingParticipant, diff --git a/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto b/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto index 946b23f077..dba27f4fb0 100644 --- a/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto +++ b/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto @@ -51,15 +51,3 @@ message StoreId { Temporary temporary = 3; } } - -message SynchronizerPredecessor { - // Id of the predecessor. - string predecessor_physical_id = 1; - - // When the upgrade happened/is supposed to happen. - google.protobuf.Timestamp upgrade_time = 2; - - // Whether the participant is performing a late synchronizer upgrade. - // default: false - bool is_late_upgrade = 3; -} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/LoggingConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/LoggingConfig.scala index ecb658ac84..3e79268344 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/LoggingConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/LoggingConfig.scala @@ -32,8 +32,7 @@ import com.digitalasset.canton.config final case class LoggingConfig( api: ApiLoggingConfig = ApiLoggingConfig(), eventDetails: Boolean = false, - // TODO(#10058) Make the Canton logic hide secrets and replace our own config dump by this. - logConfigOnStartup: Boolean = false, + logConfigOnStartup: Boolean = true, logConfigWithDefaults: Boolean = false, logSlowFutures: Boolean = false, delayLoggingThreshold: config.NonNegativeFiniteDuration = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/RefinedNonNegativeDuration.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/RefinedNonNegativeDuration.scala index db8747e712..06a35eb2b8 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/RefinedNonNegativeDuration.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/RefinedNonNegativeDuration.scala @@ -145,8 +145,6 @@ trait RefinedNonNegativeDurationCompanion[D <: RefinedNonNegativeDuration[D]] { def tryFromJavaDuration(duration: java.time.Duration): D = tryFromDuration(Duration.fromNanos(duration.toNanos)) - def ofMicros(millis: Long): D = apply(Duration(millis, TimeUnit.MICROSECONDS)) - def ofMillis(millis: Long): D = apply(Duration(millis, TimeUnit.MILLISECONDS)) def ofSeconds(secs: Long): D = apply(Duration(secs, TimeUnit.SECONDS)) @@ -389,7 +387,8 @@ object NonNegativeFiniteDuration s"Cannot convert `$input` to a non-negative finite duration: $reason" } - implicit val nonNegativeFiniteDurationReader: ConfigReader[NonNegativeFiniteDuration] = + private[canton] implicit val nonNegativeFiniteDurationReader + : ConfigReader[NonNegativeFiniteDuration] = ConfigReader.fromString[NonNegativeFiniteDuration] { str => (for { duration <- strToFiniteDuration(str) @@ -397,7 +396,8 @@ object NonNegativeFiniteDuration } yield nonNegativeFiniteDuration).leftMap(NonNegativeFiniteDurationError(str, _)) } - implicit val nonNegativeFiniteDurationWriter: ConfigWriter[NonNegativeFiniteDuration] = + private[canton] implicit val nonNegativeFiniteDurationWriter + : ConfigWriter[NonNegativeFiniteDuration] = // avoid pretty printing by converting the underlying value to string ConfigWriter.toString(_.underlying.toString) } @@ -444,7 +444,7 @@ object PositiveFiniteDuration extends RefinedNonNegativeDurationCompanion[Positi s"Cannot convert `$input` to a positive finite duration: $reason" } - implicit val positiveFiniteDurationReader: ConfigReader[PositiveFiniteDuration] = + private[canton] implicit val positiveFiniteDurationReader: ConfigReader[PositiveFiniteDuration] = ConfigReader.fromString[PositiveFiniteDuration] { str => (for { duration <- strToFiniteDuration(str) @@ -452,7 +452,7 @@ object PositiveFiniteDuration extends RefinedNonNegativeDurationCompanion[Positi } yield positiveFiniteDuration).leftMap(PositiveFiniteDurationError(str, _)) } - implicit val positiveFiniteDurationWriter: ConfigWriter[PositiveFiniteDuration] = + private[canton] implicit val positiveFiniteDurationWriter: ConfigWriter[PositiveFiniteDuration] = // avoid pretty printing by converting the underlying value to string ConfigWriter.toString(_.underlying.toString) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala index e263efee74..54e200bd1d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala @@ -448,7 +448,7 @@ object DbConfig { private val stableDir = "stable" private val devDir = "dev" private val tableSettingsDir = "table_settings" - private val basePostgresMigrationsPath: String = "classpath:db/migration/canton-network/postgres/" + private val basePostgresMigrationsPath: String = "classpath:db/migration/canton/postgres/" private val baseH2MigrationsPath: String = "classpath:db/migration/canton/h2/" val postgresMigrationsPathStable: String = basePostgresMigrationsPath + stableDir val h2MigrationsPathStable: String = baseH2MigrationsPath + stableDir diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala index bb612bd011..49524eee86 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala @@ -794,7 +794,7 @@ object SigningKeyUsage { * topology requests such as OwnerToKeyMappings and PartyToKeyMappings. Generally, any key not * intended for namespace or identity delegation will have this usage automatically assigned. */ - case object ProofOfOwnership extends SigningKeyUsage { + private case object ProofOfOwnership extends SigningKeyUsage { override val identifier: String = "proof-of-ownership" override val dbType: Byte = 4 override def toProtoEnum: v30.SigningKeyUsage = @@ -1263,7 +1263,7 @@ object SigningKeyPair { final case class SigningPublicKey private ( format: CryptoKeyFormat, - val key: ByteString, + protected[crypto] val key: ByteString, keySpec: SigningKeySpec, usage: NonEmpty[Set[SigningKeyUsage]], override protected val dataForFingerprintO: Option[ByteString], @@ -1503,7 +1503,7 @@ object SigningPublicKeyWithName { final case class SigningPrivateKey private ( id: Fingerprint, format: CryptoKeyFormat, - val key: ByteString, + protected[crypto] val key: ByteString, keySpec: SigningKeySpec, usage: NonEmpty[Set[SigningKeyUsage]], )( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala index 5e4a97bf9d..b704a21306 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala @@ -318,7 +318,7 @@ class JcePureCrypto( SymmetricKey.create(CryptoKeyFormat.Raw, bytes.unwrap, scheme) } - override def signBytes( + override protected[crypto] def signBytes( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/data/CantonTimestamp.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/data/CantonTimestamp.scala index 6e93405bcc..cb8037b3cf 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/data/CantonTimestamp.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/data/CantonTimestamp.scala @@ -96,21 +96,9 @@ object CantonTimestamp { ts <- LfTimestamp.fromInstant(instant).left.map(err => TimestampConversionError(err)) } yield new CantonTimestamp(ts) - def tryFromProtoTimestamp(ts: ProtoTimestamp): CantonTimestamp = - fromProtoTimestamp(ts).fold( - err => throw new IllegalArgumentException(err.message), - identity, - ) - def fromProtoPrimitive(ts: Long): ParsingResult[CantonTimestamp] = LfTimestamp.fromLong(ts).bimap(TimestampConversionError.apply, new CantonTimestamp(_)) - def tryFromProtoPrimitive(ts: Long): CantonTimestamp = - fromProtoPrimitive(ts).fold( - err => throw new IllegalArgumentException(err.message), - identity, - ) - def ofEpochSecond(seconds: Long): CantonTimestamp = { // Explicitly check the bounds here to avoid overflows due to the scaling by 1M require( @@ -135,16 +123,6 @@ object CantonTimestamp { def fromInstant(i: Instant): Either[String, CantonTimestamp] = LfTimestamp.fromInstant(i).map(t => new CantonTimestamp(t)) - def tryFromInstant(time: Instant) = - fromInstant(time) - .fold( - err => - throw new IllegalArgumentException( - s"cannot parse the instant $time for the topology snapshot: $err" - ), - identity, - ) - def fromDate(javaDate: Date): Either[String, CantonTimestamp] = for { instant <- TryUtil.tryCatchInterrupted(javaDate.toInstant).toEither.leftMap(_.getMessage) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/HasLifeCycleScope.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/HasLifeCycleScope.scala index 82a4c5f715..6cbccb8a74 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/HasLifeCycleScope.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/HasLifeCycleScope.scala @@ -49,7 +49,8 @@ sealed trait HasLifeCycleScope extends Any { * For without either of the bounds, [[scala.Any]] or [[scala.Nothing]] would be valid * refinements. */ - type ContextLifeCycleScopeDiscriminator >: ContextLifeCycleScopeDiscriminatorLower <: ContextLifeCycleScopeDiscriminatorUpper + type ContextLifeCycleScopeDiscriminator >: ContextLifeCycleScopeDiscriminatorLower + <: ContextLifeCycleScopeDiscriminatorUpper /** The discriminator for an individual instance's own [[LifeCycleScope]]. It is deliberately left * unspecified so that scalac cannot derive type equalities for the discriminator across @@ -64,7 +65,8 @@ sealed trait HasLifeCycleScope extends Any { * For without either of the bounds, [[scala.Any]] or [[scala.Nothing]] would be valid * refinements. */ - protected type OwnLifeCycleScopeDiscriminator >: OwnLifeCycleScopeDiscriminatorLower <: OwnLifeCycleScopeDiscriminatorUpper + protected type OwnLifeCycleScopeDiscriminator >: OwnLifeCycleScopeDiscriminatorLower + <: OwnLifeCycleScopeDiscriminatorUpper /** Converts a [[ContextLifeCycleScope]] from this instance into a [[ContextLifeCycleScope]] of * another instance of [[HasLifeCycleScope]], possibly adding this [[HasLifeCycleScope]]'s diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/OnShutdownRunner.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/OnShutdownRunner.scala index ed38b357d0..0d6c6e35cc 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/OnShutdownRunner.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/OnShutdownRunner.scala @@ -3,21 +3,16 @@ package com.digitalasset.canton.lifecycle -import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.UnlessShutdown.{AbortedDueToShutdown, Outcome} import com.digitalasset.canton.logging.{ErrorLoggingContext, TracedLogger} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.TryUtil.* import com.digitalasset.canton.util.TwoPhasePriorityAccumulator -import java.util.concurrent.atomic.AtomicReference import scala.util.Try trait OnShutdownRunner extends HasRunOnClosing { this: AutoCloseable => - private val status: AtomicReference[OnShutdownRunner.Status] = new AtomicReference( - OnShutdownRunner.Status.Running - ) private val onShutdownTasks: TwoPhasePriorityAccumulator[RunOnClosing, Unit] = new TwoPhasePriorityAccumulator[RunOnClosing, Unit](Some(_.done)) @@ -27,25 +22,13 @@ trait OnShutdownRunner extends HasRunOnClosing { this: AutoCloseable => * flag to the retry lib or you really know what you're doing, prefer `performUnlessClosing` and * friends. */ - override def isClosing: Boolean = status.get() != OnShutdownRunner.Status.Running + override def isClosing: Boolean = !onShutdownTasks.isAccumulating - def runOnShutdownWithPriority_( - task: RunOnClosing - ): Unit = { - runOnShutdownWithPriority(task, 0).discard - } - - override def runOnClose(task: RunOnClosing): UnlessShutdown[LifeCycleRegistrationHandle] = { - val priority = 10 - runOnShutdownWithPriority(task, priority) - } - - private def runOnShutdownWithPriority[T](task: RunOnClosing, priority: Int) = { - onShutdownTasks.accumulate(task, priority) match { + override def runOnClose(task: RunOnClosing): UnlessShutdown[LifeCycleRegistrationHandle] = + onShutdownTasks.accumulate(task, 0) match { case Right(handle) => Outcome(new LifeCycleManager.LifeCycleRegistrationHandleImpl(handle)) case Left(_) => AbortedDueToShutdown } - } override protected[this] def runTaskUnlessDone( task: RunOnClosing @@ -67,8 +50,6 @@ trait OnShutdownRunner extends HasRunOnClosing { this: AutoCloseable => */ protected[this] override def close(): Unit = { import TraceContext.Implicits.Empty.* - status.set(OnShutdownRunner.Status.Closing) - logger.debug("Initiating shutdown.")(TraceContext.empty) val firstCallToClose = onShutdownTasks.stopAccumulating(()).isEmpty if (firstCallToClose) { @@ -81,15 +62,6 @@ trait OnShutdownRunner extends HasRunOnClosing { this: AutoCloseable => // TODO(i8594): Ensure we call close only once } } - - /** Marks this instance as closing, but does *not* initiate shutdown tasks. - * Used to mark all CN RetryProviders as closing before cancelling any running tasks, - * thus avoiding any spurious warns/errors that may happen due to cancellation exceptions being thrown. - */ - def setAsClosing(): Unit = { - logger.debug("Setting as closing.")(TraceContext.empty) - status.set(OnShutdownRunner.Status.SetAsClosing) - } } object OnShutdownRunner { @@ -101,11 +73,4 @@ object OnShutdownRunner { override protected def onFirstClose(): Unit = () override def close(): Unit = super.close() } - - private sealed trait Status - private object Status { - case object Running extends Status - case object SetAsClosing extends Status - case object Closing extends Status - } } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/PerformUnlessClosing.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/PerformUnlessClosing.scala index e512e70a1a..cd43770198 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/PerformUnlessClosing.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/lifecycle/PerformUnlessClosing.scala @@ -51,11 +51,6 @@ trait PerformUnlessClosing extends OnShutdownRunner with HasSynchronizeWithReade )(implicit ec: ExecutionContext, traceContext: TraceContext): FutureUnlessShutdown[A] = synchronizeWithClosing(name)(FutureUnlessShutdown.outcomeF(f)) -// def performUnlessClosingEitherT[E, R](name: String, onClosing: => E)( -// etf: => EitherT[Future, E, R] -// )(implicit ec: ExecutionContext, traceContext: TraceContext): EitherT[Future, E, R] = -// EitherT(performUnlessClosingF(name)(etf.value).unwrap.map(_.onShutdown(Left(onClosing)))) - protected def onClosed(): Unit = () protected def onCloseFailure(e: Throwable): Unit = throw e diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/logging/pretty/PrettyInstances.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/logging/pretty/PrettyInstances.scala index 6e7e31e8ac..b4c25d1c21 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/logging/pretty/PrettyInstances.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/logging/pretty/PrettyInstances.scala @@ -29,10 +29,10 @@ import slick.util.{DumpInfo, Dumpable} import java.lang.Long as JLong import java.net.URI -import java.time.{Instant, Duration as JDuration} +import java.time.{Duration as JDuration, Instant} import java.util.UUID import scala.annotation.nowarn -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{Duration, FiniteDuration} /** Collects instances of [[Pretty]] for common types. */ @@ -103,7 +103,7 @@ trait PrettyInstances { elements => treeOfIterable("Map", elements.map { case (k, v) => Tree.Infix(k.toTree, "->", v.toTree) }) - def treeOfIterable[T: Pretty](prefix: String, elements: Iterable[T]): Tree = + private def treeOfIterable[T: Pretty](prefix: String, elements: Iterable[T]): Tree = if (elements.sizeCompare(1) == 0) { elements.iterator.next().toTree } else { @@ -115,7 +115,14 @@ trait PrettyInstances { _.toString.substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase ) - implicit def prettyDuration: Pretty[Duration] = prettyOfString(_.toString) + implicit def prettyDuration: Pretty[Duration] = { + case fduration: FiniteDuration => + import scala.jdk.DurationConverters.ScalaDurationOps + prettyJDuration.treeOf( + fduration.toJava + ) + case infDuration: Duration.Infinite => Tree.Literal(infDuration.toString) + } implicit def prettyURI: Pretty[URI] = prettyOfString(_.toString) @@ -165,7 +172,7 @@ trait PrettyInstances { implicit val prettyNodeId: Pretty[LfNodeId] = prettyOfParam(_.index) - protected def prettyUidString(partyStr: String): String = + private def prettyUidString(partyStr: String): String = UniqueIdentifier.fromProtoPrimitive_(partyStr) match { case Right(uid) => uid.show case Left(_) => partyStr diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/SynchronizerParameters.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/SynchronizerParameters.scala index dd83d4b0b3..2f7e7ecded 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/SynchronizerParameters.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/SynchronizerParameters.scala @@ -534,9 +534,9 @@ object DynamicSynchronizerParameters extends VersioningCompanion[DynamicSynchron NonNegativeInt.tryCreate(10 * 1024 * 1024) ) - val defaultConfirmationResponseTimeout: NonNegativeFiniteDuration = + private val defaultConfirmationResponseTimeout: NonNegativeFiniteDuration = NonNegativeFiniteDuration.tryOfSeconds(30) - val defaultMediatorReactionTimeout: NonNegativeFiniteDuration = + private val defaultMediatorReactionTimeout: NonNegativeFiniteDuration = NonNegativeFiniteDuration.tryOfSeconds(30) private val defaultAssignmentExclusivityTimeout: NonNegativeFiniteDuration = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ConfirmationResultMessage.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ConfirmationResultMessage.scala index 0bb5b7883f..7fec7a5e1e 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ConfirmationResultMessage.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ConfirmationResultMessage.scala @@ -94,7 +94,7 @@ case class ConfirmationResultMessage private ( object ConfirmationResultMessage extends VersioningCompanionMemoization[ - ConfirmationResultMessage + ConfirmationResultMessage, ] { override val name: String = "ConfirmationResultMessage" diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ProtocolMessage.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ProtocolMessage.scala index e2379ce82d..52ad5e714d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ProtocolMessage.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/ProtocolMessage.scala @@ -232,7 +232,13 @@ object SignedProtocolMessage )(implicit traceContext: TraceContext, ec: ExecutionContext, - ): EitherT[FutureUnlessShutdown, SyncCryptoError, SignedProtocolMessage[M]] = ??? + ): EitherT[FutureUnlessShutdown, SyncCryptoError, SignedProtocolMessage[M]] = { + val typedMessage = TypedSignedProtocolMessageContent(message) + + mkSignature(typedMessage, cryptoApi, signingTimestampOverrides).map { signature => + SignedProtocolMessage(typedMessage, NonEmpty(Seq, signature)) + } + } /** @param signingTimestampOverrides * Optional overrides for selecting an approximate signing timestamp and validity end, used to diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/SetTrafficPurchasedMessage.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/SetTrafficPurchasedMessage.scala index 74122e38c8..9910e0157d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/SetTrafficPurchasedMessage.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/protocol/messages/SetTrafficPurchasedMessage.scala @@ -63,7 +63,7 @@ final case class SetTrafficPurchasedMessage private ( object SetTrafficPurchasedMessage extends VersioningCompanionMemoization[ - SetTrafficPurchasedMessage + SetTrafficPurchasedMessage, ] { override val name: String = "SetTrafficPurchasedMessage" diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala index b7e6fe8dac..8e6c44c510 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala @@ -63,6 +63,7 @@ class DbMigrations( .locations(dbConfig.buildMigrationsPaths(alphaVersionSupport)*) .dataSource(dataSource) .cleanDisabled(!dbConfig.parameters.unsafeCleanOnValidationError) + .cleanOnValidationError(dbConfig.parameters.unsafeCleanOnValidationError) .baselineOnMigrate(dbConfig.parameters.unsafeBaselineOnMigrate) .lockRetryCount(60) .load() diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbVersionCheck.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbVersionCheck.scala index 77c6b18078..9ea6af15cd 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbVersionCheck.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbVersionCheck.scala @@ -33,7 +33,7 @@ object DbVersionCheck extends HasLoggerName { val either: Either[DbMigrations.Error, Unit] = profile match { case Profile.Postgres(_) => - val expectedPostgresVersions = NonEmpty(Seq, 14, 15, 16, 17, 18) + val expectedPostgresVersions = NonEmpty(Seq, 14, 15, 16, 17) val expectedPostgresVersionsStr = s"${(expectedPostgresVersions.dropRight(1)).mkString(", ")}, or ${expectedPostgresVersions .takeRight(1) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala index d2dd73d0cc..b17dad0867 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala @@ -263,8 +263,7 @@ object SequencerClientFactory { initialDelay = config.startupConnectionRetryDelay.asFiniteApproximation, maxDelay = config.maxConnectionRetryDelay.asFiniteApproximation, "Traffic State Initialization", - longDescription = - s"Initialize traffic state from a BFT read with threshold ${sequencerConnections.sequencerTrustThreshold} from ${sequencerConnections.connections.length} total connections", + s"Initialize traffic state from a BFT read with threshold ${sequencerConnections.sequencerTrustThreshold} from ${sequencerConnections.connections.length} total connections", retryLogLevel = Some(Level.INFO), ) .unlessShutdown( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala index fc44499167..4228b3fd0a 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/traffic/TrafficPurchasedSubmissionHandler.scala @@ -141,7 +141,7 @@ class TrafficPurchasedSubmissionHandler( threshold = sequencerGroup.threshold, protocolVersion = protocolVersion, ) - setTrafficPurchasedMessage: SetTrafficPurchasedMessage = SetTrafficPurchasedMessage.apply( + setTrafficPurchasedMessage = SetTrafficPurchasedMessage.apply( member, serial, totalTrafficPurchased, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/Member.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/Member.scala index 288c4cddf9..a397f7e171 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/Member.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/Member.scala @@ -363,9 +363,6 @@ object ParticipantId { def apply(identifier: String, namespace: Namespace): ParticipantId = ParticipantId(UniqueIdentifier.tryCreate(identifier, namespace)) - def apply(uid: UniqueIdentifier): ParticipantId = - new ParticipantId(uid) - /** create a participant from a string * * used in testing diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala index 68cff0bbaf..daa89df415 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManagerError.scala @@ -155,33 +155,6 @@ object TopologyManagerError extends TopologyManagerErrorGroup { ) extends CantonError.Impl( cause = "No synchronizer store available." ) - with TopologyManagerError - - final case class MultipleSynchronizerStoresFound(storeIds: Seq[TopologyStoreId])(implicit - val loggingContext: ErrorLoggingContext - ) extends CantonError.Impl( - cause = - s"Multiple topology stores found for the provided storeId: ${storeIds.mkString(", ")}." - ) - with TopologyManagerError - - } - - @Explanation( - "This error indicates that the topology has not yet been initialized or is in the process of being initialized." - ) - @Resolution( - "The failed operation can be retried after the store has been fully initialized, for example after the logical synchronizer upgrade has completed." - ) - object TopologyStoreNotInitialized - extends ErrorCode( - id = "TOPOLOGY_STORE_NOT_INITIALIZED", - ErrorCategory.InvalidGivenCurrentSystemStateOther, - ) { - final case class Failure(storeId: TopologyStoreId)(implicit - val loggingContext: ErrorLoggingContext - ) extends CantonError.Impl(cause = s"Topology store '$storeId' is not yet initialized.") - with TopologyManagerError } @Explanation( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/cache/TopologyStateWriteThroughCache.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/cache/TopologyStateWriteThroughCache.scala index c5e16a59fa..7602ee6b67 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/cache/TopologyStateWriteThroughCache.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/cache/TopologyStateWriteThroughCache.scala @@ -532,7 +532,7 @@ class TopologyStateWriteThroughCache( .filter(tx => tx.stored.transaction.isProposal && maxSerial.forall(_ < tx.stored.serial) ) - .map(_.stored.transaction.transaction.hash) + .map(_.stored.transaction.transaction.hash), ) } @@ -1332,9 +1332,8 @@ object TopologyStateWriteThroughCache { tail.iterator.takeWhile(_.validUntil.exists(_.value >= asOf.value)) headAndTail.filter { tx => tx.isActiveAsOf(asOf = asOf, asOfInclusive = asOfInclusive) && - !tx.transaction.isProposal && op.forall( - _ == tx.transaction.operation - ) && tx.rejectionReason.isEmpty + !tx.transaction.isProposal && op.forall(_ == tx.transaction.operation) + && tx.rejectionReason.isEmpty }.toSeq } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionCollection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionCollection.scala index a50623956e..d402d2beb9 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionCollection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyTransactionCollection.scala @@ -114,7 +114,7 @@ final case class StoredTopologyTransactions[+Op <: TopologyChangeOp, +M <: Topol object StoredTopologyTransactions extends HasVersionedMessageCompanion[ - StoredTopologyTransactions[TopologyChangeOp, TopologyMapping] + StoredTopologyTransactions[TopologyChangeOp, TopologyMapping], ] { type GenericStoredTopologyTransactions = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala index bf1f90cf4c..54968c3005 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala @@ -547,11 +547,12 @@ class DbTopologyStore[+StoreId <: TopologyStoreId]( // PartyToParticipant filtering val ptpFilter: SQLActionBuilder = - sql"(transaction_type = ${PartyToParticipant.code}" ++ conditionalAppend( - filterParty, - filterPartyIdentifier, - filterPartyNamespaceO, - ).toList ++ sql")" + sql"(transaction_type = ${PartyToParticipant.code}" + ++ conditionalAppend( + filterParty, + filterPartyIdentifier, + filterPartyNamespaceO, + ).toList ++ sql")" val adminPartyConditions = // SynchronizerTrustCertificate filtering for admin parties party diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/memory/InMemoryTopologyStore.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/memory/InMemoryTopologyStore.scala index 0c8032f338..936fcd2457 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/memory/InMemoryTopologyStore.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/memory/InMemoryTopologyStore.scala @@ -115,9 +115,10 @@ class InMemoryTopologyStore[+StoreId <: TopologyStoreId]( lock.exclusive { topologyTransactionStore .filter(x => - x.from.value < asOfExclusive.value && x.rejected.isEmpty && x.until.forall( - _.value >= asOfExclusive.value - ) && filter(x) + x.from.value < asOfExclusive.value + && x.rejected.isEmpty + && x.until.forall(_.value >= asOfExclusive.value) + && filter(x) ) .map(_.transaction) .toSeq @@ -214,7 +215,8 @@ class InMemoryTopologyStore[+StoreId <: TopologyStoreId]( topologyTransactionStore .filter { entry => itemsMap.get(entry.indexKey).exists { validUntil => - entry.rejected.isEmpty && entry.until.forall(ts => ts >= validUntil) + entry.rejected.isEmpty + && entry.until.forall(ts => ts >= validUntil) } } .sortBy(c => (c.until.map(_.value).getOrElse(CantonTimestamp.MaxValue), c.batchIdx)) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/ErrorUtil.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/ErrorUtil.scala index 85d0d11002..7cc0d36b3c 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/ErrorUtil.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/ErrorUtil.scala @@ -6,19 +6,12 @@ package com.digitalasset.canton.util import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.ErrorLoggingContext -import io.grpc.{Status, StatusRuntimeException} import scala.concurrent.Future import scala.util.Failure import scala.util.control.NonFatal object ErrorUtil { - def internalErrorGrpc(msg: String)(implicit loggingContext: ErrorLoggingContext): Nothing = { - val t = new StatusRuntimeException(Status.INTERNAL.withDescription(msg)) - logInternalError(t) - throw t - } - /** Logs and rethrows any throwable. */ def withThrowableLogging[T](action: => T, valueOnThrowable: Option[T] = None)(implicit diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala index 9e39cd78d1..8d0502fb42 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala @@ -1055,15 +1055,10 @@ object PekkoUtil extends HasLoggerName { sealed abstract class ContextualizedFlowOpsImpl { type ContextualizedFlowOps[+Context[+_], +A, +Mat] <: FlowOps[Context[A], Mat] - type ContextualizedSource[+Context[+_], +A, +Mat] <: Source[ - Context[A], - Mat, - ] & ContextualizedFlowOps[Context, A, Mat] - type ContextualizedFlow[+Context[+_], -A, +B, +Mat] <: Flow[ - A, - Context[B], - Mat, - ] & ContextualizedFlowOps[Context, B, Mat] + type ContextualizedSource[+Context[+_], +A, +Mat] + <: Source[Context[A], Mat] & ContextualizedFlowOps[Context, A, Mat] + type ContextualizedFlow[+Context[+_], -A, +B, +Mat] + <: Flow[A, Context[B], Mat] & ContextualizedFlowOps[Context, B, Mat] def substFlowOps[K[_[_]], Context[+_], Mat]( ff: K[Lambda[a => FlowOps[Context[a], Mat]]] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/retry/Policy.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/retry/Policy.scala index 434bbeddd7..3aae2e71d6 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/retry/Policy.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/retry/Policy.scala @@ -23,7 +23,6 @@ import org.slf4j.event.Level import scala.concurrent.duration.{Duration, FiniteDuration} import scala.concurrent.{ExecutionContext, Future} -import scala.jdk.DurationConverters.* import scala.util.control.NonFatal import scala.util.{Failure, Try} @@ -99,7 +98,6 @@ abstract class RetryWithDelay( actionable: Option[String], // How to mitigate the error initialDelay: FiniteDuration, totalMaxRetries: Int, - resetRetriesAfter: Option[FiniteDuration], hasSynchronizeWithClosing: HasSynchronizeWithClosing, retryLogLevel: Option[Level], suspendRetries: Eval[FiniteDuration], @@ -170,9 +168,6 @@ abstract class RetryWithDelay( import LoggerUtil.logOnThrow - val clock = java.time.Clock.systemUTC() - import java.time.Duration as JDuration, clock.instant as now - def runTask(): Future[T] = Future.fromTry(Try(task)).flatten def run( @@ -182,9 +177,7 @@ abstract class RetryWithDelay( retriesOfLastErrorKind: Int, delay: FiniteDuration, ): Future[RetryOutcome[T]] = logOnThrow { - val startedAt = now() previousResult.transformWith { x => - val finishedAt = now() logOnThrow( x match { case succ @ util.Success(result) if success.predicate(result) => @@ -258,10 +251,7 @@ abstract class RetryWithDelay( } LoggerUtil.logAtLevel( level, - messageOfOutcome( - outcome, - show"${change}Retrying after a number of $retriesOfErrorKind failures, and after $delay.", - ), + messageOfOutcome(outcome, show"${change}Retrying after $delay."), // No need to log the exception in the outcome, as this has been logged by retryable.retryOk. ) @@ -291,22 +281,13 @@ abstract class RetryWithDelay( // and therefore yield the termination reason `Shutdown`. Future.fromTry(outcome) } - import scala.math.Ordering.Implicits.* - val (nextTotalRetries, nextDelayIs) = - if ( - resetRetriesAfter - .fold(false)(_.toJava <= JDuration.between(startedAt, finishedAt)) - ) - (0, initialDelay) - else - (totalRetries + 1, nextDelay(totalRetries + 1, delay)) FutureUnlessShutdown.outcomeF( run( previousResult = nextRunF, - totalRetries = nextTotalRetries, + totalRetries = totalRetries + 1, lastErrorKind = Some(errorKind), retriesOfLastErrorKind = retriesOfErrorKind + 1, - delay = nextDelayIs, + delay = nextDelay(totalRetries + 1, delay), ) )(executionContext) } @@ -421,7 +402,6 @@ final case class Directly( None, Duration.Zero, maxRetries, - resetRetriesAfter = None, hasSynchronizeWithClosing, retryLogLevel, suspendRetries, @@ -448,7 +428,6 @@ final case class Pause( actionable, delay, maxRetries, - resetRetriesAfter = None, hasSynchronizeWithClosing, retryLogLevel, suspendRetries, @@ -497,7 +476,6 @@ final case class Backoff( initialDelay: FiniteDuration, maxDelay: Duration, operationName: String, - resetRetriesAfter: Option[FiniteDuration] = None, longDescription: String = "", actionable: Option[String] = None, retryLogLevel: Option[Level] = None, @@ -510,7 +488,6 @@ final case class Backoff( actionable, initialDelay, maxRetries, - resetRetriesAfter, hasSynchronizeWithClosing, retryLogLevel, suspendRetries, @@ -537,7 +514,6 @@ object Backoff { config.initialDelay.underlying, config.maxDelay.unwrap, operationName, - None, longDescription, actionable, retryLogLevel, diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/data/ConcurrentHMap.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/data/ConcurrentHMap.scala index 0d598fcdc5..0d880d2653 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/data/ConcurrentHMap.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/data/ConcurrentHMap.scala @@ -5,7 +5,6 @@ package com.digitalasset.canton.data import com.digitalasset.canton.discard.Implicits.DiscardOps -import scala.annotation.unused import scala.collection.concurrent import scala.collection.concurrent.TrieMap @@ -57,8 +56,7 @@ object ConcurrentHMap { private[data] class ConcurrentHMapPartiallyApplied[R[_, _]](private val dummy: Boolean) extends AnyVal { - - def apply[K, V](values: (K, V)*)(implicit @unused ev: R[K, V]) = + def apply[K, V](values: (K, V)*)(implicit ev: R[K, V]) = new ConcurrentHMap[R](TrieMap.from(values)) } } diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/TopologyStoreId.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/TopologyStoreId.scala index d3cfce8048..fb324661bc 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/TopologyStoreId.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/TopologyStoreId.scala @@ -110,7 +110,7 @@ object TopologyStoreId { physical => Right(store.TopologyStoreId.SynchronizerStore(physical)), ) - def logicalSynchronizerId: SynchronizerId = id.fold(identity, _.logical) + private[canton] def logicalSynchronizerId: SynchronizerId = id.fold(identity, _.logical) } object Synchronizer { diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala index e6b78de10d..0b72438085 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala @@ -270,43 +270,6 @@ object GrpcStreamingUtils { requestComplete.future } - def streamToServerChunked[Req, Resp]( - load: StreamObserver[Resp] => StreamObserver[Req], - requests: Seq[Req], - ): Future[Resp] = { - val requestComplete = Promise[Resp]() - val ref = new AtomicReference[Option[Resp]](None) - - val responseObserver = new StreamObserver[Resp] { - override def onNext(value: Resp): Unit = - ref.set(Some(value)) - - override def onError(t: Throwable): Unit = requestComplete.failure(t) - - override def onCompleted(): Unit = - ref.get() match { - case Some(response) => requestComplete.success(response) - case None => - requestComplete.failure( - io.grpc.Status.CANCELLED - .withDescription("Server completed the request before providing a response") - .asRuntimeException() - ) - } - - } - val requestObserver = load(responseObserver) - - requests - .foreach { request => - blocking { - requestObserver.onNext(request) - } - } - requestObserver.onCompleted() - requestComplete.future - } - def streamToClient[T]( responseF: OutputStream => Future[Unit], responseObserver: StreamObserver[T], diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala index c1d778a296..4a71076725 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/protocol/ExampleTransactionFactory.scala @@ -2140,9 +2140,7 @@ class ExampleTransactionFactory( mkMetadata(seeds.filter(_._1 == LfNodeId(4))), Map.empty, ), - Witnesses( - NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees) - ), + Witnesses(NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees)), ), ) @@ -2652,9 +2650,7 @@ class ExampleTransactionFactory( mkMetadata(seeds.filter(_._1 == LfNodeId(6))), Map.empty, ), - Witnesses( - NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees) - ), + Witnesses(NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees)), ), ( transactionViewTree11, @@ -2663,9 +2659,7 @@ class ExampleTransactionFactory( mkMetadata(seeds.filter(seed => Seq(7, 8).map(LfNodeId.apply).contains(seed._1))), Map.empty, ), - Witnesses( - NonEmpty(List, transactionViewTree11.informees, transactionViewTree1.informees) - ), + Witnesses(NonEmpty(List, transactionViewTree11.informees, transactionViewTree1.informees)), ), ( transactionViewTree110, @@ -3297,9 +3291,7 @@ class ExampleTransactionFactory( mkMetadata(seeds.filter(seed => Seq(2, 3).map(LfNodeId.apply).contains(seed._1))), Map.empty, ), - Witnesses( - NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees) - ), + Witnesses(NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees)), ), ( transactionViewTree100, @@ -3324,9 +3316,7 @@ class ExampleTransactionFactory( mkMetadata(seeds.filter(seed => Seq(5, 6).map(LfNodeId.apply).contains(seed._1))), Map.empty, ), - Witnesses( - NonEmpty(List, transactionViewTree11.informees, transactionViewTree1.informees) - ), + Witnesses(NonEmpty(List, transactionViewTree11.informees, transactionViewTree1.informees)), ), ( transactionViewTree110, @@ -3729,9 +3719,7 @@ class ExampleTransactionFactory( mkMetadata(seeds.filter(seed => Seq(3, 4).map(LfNodeId.apply).contains(seed._1))), Map.empty, ), - Witnesses( - NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees) - ), + Witnesses(NonEmpty(List, transactionViewTree10.informees, transactionViewTree1.informees)), ), ) diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala index 3a3a222a08..7c41fb0799 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala @@ -28,7 +28,8 @@ trait DbTest with FlagCloseable with HasCloseContext // not used here, but required by most tests. So extending it for convenience. with HasExecutionContext - with NamedLogging { + with NamedLogging + with DbStorage.Implicits { this: Suite => /** Flag to define the migration mode for the schemas */ @@ -100,12 +101,9 @@ trait DbTest } } - private def cleanup()(implicit tc: TraceContext): Unit = { + private def cleanup()(implicit tc: TraceContext): Unit = // Use the underlying storage for clean-up operations, so we don't run clean-ups twice - // cleanDB is usually implemented by a TRUNCATE statement, which can be very slow, - // we therefore use a long timeout. - Await.result(cleanDb(storage.underlying), 120.seconds) - } + Await.result(cleanDb(storage.underlying), 10.seconds) } /** Run db test against h2 */ diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala index 08d0509397..7b4d1a0aa2 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficControlProcessorTest.scala @@ -59,7 +59,7 @@ class TrafficControlProcessorTest extends AnyWordSpec with BaseTest with HasExec private def mkSetTrafficPurchased( signatureO: Option[Signature] = None ): SignedProtocolMessage[SetTrafficPurchasedMessage] = { - val setTrafficPurchased: SetTrafficPurchasedMessage = SetTrafficPurchasedMessage( + val setTrafficPurchased = SetTrafficPurchasedMessage( participantId, PositiveInt.one, NonNegativeLong.tryCreate(100), diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala deleted file mode 100644 index 5cf4821f09..0000000000 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.integration - -import com.digitalasset.canton.config.{SharedCantonConfig, TestingConfigInternal} -import com.digitalasset.canton.environment.Environment -import com.digitalasset.canton.logging.NamedLoggerFactory - -/** Definition of how a environment should be configured and setup. - * @param baseConfig - * the base config to use (typically loaded from a pre-canned config file or sample) - * @param testingConfig - * the testing specifics bits of the config - * @param setups - * a function to configure the environment before tests can be run. - * @param teardown - * a function to perform cleanup after the environment has been destroyed. - * @param configTransforms - * transforms to perform on the base configuration before starting the environment (typically - * making ports unique or some other specialization for the particular tests you're running) - */ -abstract class BaseEnvironmentDefinition[C <: SharedCantonConfig[ - C -], E <: Environment[C]]( - val baseConfig: C, - val testingConfig: TestingConfigInternal, - val setups: List[BaseTestConsoleEnvironment[C, E] => Unit] = Nil, - val teardown: Unit => Unit = _ => (), - val configTransforms: Seq[C => C], -) { - - /** Create a canton configuration by applying the configTransforms to the base config. Some - * transforms may have side-effects (such as incrementing the next available port number) so only - * do before constructing an environment. - */ - def generateConfig: C = - configTransforms.foldLeft(baseConfig)((config, transform) => transform(config)) - - def createTestConsole( - environment: E, - loggerFactory: NamedLoggerFactory, - ): BaseTestConsoleEnvironment[C, E] -} diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala index 71191552b5..67dc87bf11 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseIntegrationTest.scala @@ -13,8 +13,6 @@ import com.digitalasset.canton.{ TestPredicateFiltersFixtureAnyWordSpec, config, } -import com.digitalasset.canton.config.SharedCantonConfig -import com.digitalasset.canton.environment.Environment import org.scalactic.source import org.scalactic.source.Position import org.scalatest.wordspec.FixtureAnyWordSpec @@ -26,9 +24,9 @@ import scala.jdk.CollectionConverters.* /** A highly opinionated base trait for writing integration tests interacting with a canton * environment using console commands. Tests must mixin a further [[EnvironmentSetup]] * implementation to define when the canton environment is setup around the individual tests: - * - [[BaseIsolatedEnvironments]] will construct a fresh environment for each test. - * - [[BaseSharedEnvironment]] will construct only a single environment and reuse this for each - * test executed in the test class. + * - [[IsolatedEnvironments]] will construct a fresh environment for each test. + * - [[SharedEnvironment]] will construct only a single environment and reuse this for each test + * executed in the test class. * * Test classes must override [[HasEnvironmentDefinition.environmentDefinition]] to describe how * they would like their environment configured. @@ -55,17 +53,22 @@ import scala.jdk.CollectionConverters.* * All integration tests must be located in package [[com.digitalasset.canton.integration.tests]] * or a subpackage thereof. This is required to correctly compute unit test coverage. */ -trait BaseIntegrationTest[C <: SharedCantonConfig[C], E <: Environment[C]] +private[integration] trait BaseIntegrationTest extends FixtureAnyWordSpec with BaseTest with RepeatableTestSuiteTest with PartyTopologyUtils with TestPredicateFiltersFixtureAnyWordSpec { - this: EnvironmentSetup[C, E] => + self: EnvironmentSetup => - type FixtureParam = BaseTestConsoleEnvironment[C, E] + type FixtureParam = TestConsoleEnvironment override protected def withFixture(test: OneArgTest): Outcome = { + val integrationTestPackage = "com.digitalasset.canton.integration.tests" + getClass.getName should startWith( + integrationTestPackage + ) withClue s"\nAll integration tests must be located in $integrationTestPackage or a subpackage thereof." + super[RepeatableTestSuiteTest].withFixture(new TestWithSetup(test)) } @@ -92,13 +95,9 @@ trait BaseIntegrationTest[C <: SharedCantonConfig[C], E <: Environment[C]] within, assertions.map { assertion => (entry: LogEntry) => assertion(entry) - // `commandFailureMessage` forces the loggerName to be one of Canton's, - // but we use our custom one from splice... so we have to hack around that - entry - .copy(loggerName = "com.digitalasset.canton.integration.EnvironmentDefinition") - .commandFailureMessage + entry.commandFailureMessage succeed - }* + } *, ) /** Version of [[com.digitalasset.canton.logging.SuppressingLogger.assertThrowsAndLogs]] that is @@ -116,7 +115,7 @@ trait BaseIntegrationTest[C <: SharedCantonConfig[C], E <: Environment[C]] assertion(entry) entry.commandFailureMessage succeed - }* + } *, ) /** Similar to [[com.digitalasset.canton.console.commands.ParticipantAdministration#ping]] But @@ -147,10 +146,10 @@ trait BaseIntegrationTest[C <: SharedCantonConfig[C], E <: Environment[C]] override val pos: Option[Position] = test.pos override def apply(): Outcome = { - val environment = provideEnvironment(test.name) + val environment = provideEnvironment val testOutcome = try test.toNoArgTest(environment)() - finally testFinished(test.name, environment) + finally testFinished(environment) testOutcome } } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala index c1f989c790..97d92dd9f0 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/CommunityIntegrationTest.scala @@ -1,13 +1,12 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package com.digitalasset.canton.integration -import com.digitalasset.canton.environment.{CommunityEnvironmentFactory, CantonEnvironmentFactory} +import com.digitalasset.canton.environment.{CommunityEnvironmentFactory, EnvironmentFactory} -trait CommunityIntegrationTest extends CantonBaseIntegrationTest { - this: CantonEnvironmentSetup => +trait CommunityIntegrationTest extends BaseIntegrationTest { + this: EnvironmentSetup => - override protected val environmentFactory: CantonEnvironmentFactory = - CommunityEnvironmentFactory + override protected val environmentFactory: EnvironmentFactory = CommunityEnvironmentFactory } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConcurrentEnvironmentLimiter.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConcurrentEnvironmentLimiter.scala index 0fb30c3662..6b7c02bc0a 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConcurrentEnvironmentLimiter.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConcurrentEnvironmentLimiter.scala @@ -48,7 +48,7 @@ object ConcurrentEnvironmentLimiter extends LazyLogging { val IntegrationTestConcurrencyLimit = "canton-test.integration.concurrency" - private val concurrencyLimit: Int = System.getProperty(IntegrationTestConcurrencyLimit, "1").toInt + private val concurrencyLimit: Int = System.getProperty(IntegrationTestConcurrencyLimit, "3").toInt /** Configured to be fair so earlier started tests will be first to get environments */ private val semaphore = new Semaphore(concurrencyLimit, true) diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala index 2591ec2acd..1d72d0b560 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/ConfigTransforms.scala @@ -873,7 +873,7 @@ object ConfigTransforms { ) def defaultsForNodes: Seq[ConfigTransform] = - setProtocolVersion(ProtocolVersion.v35) :+ enableAchs + setProtocolVersion(ProtocolVersion.v34) :+ enableAchs def setTopologyTransactionRegistrationTimeout( timeout: config.NonNegativeFiniteDuration diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala index ccaa4ef008..b819c9770b 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentDefinition.scala @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package com.digitalasset.canton.integration @@ -22,11 +22,11 @@ import com.digitalasset.canton.config.{ } import com.digitalasset.canton.console.{ BaseInspection, - CantonConsoleEnvironment, + ConsoleEnvironment, InstanceReference, TestConsoleOutput, } -import com.digitalasset.canton.environment.CantonNode +import com.digitalasset.canton.environment.{CantonNode, Environment} import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -42,7 +42,6 @@ import com.digitalasset.canton.{BaseTest, SynchronizerAlias} import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.LazyLogging import monocle.macros.syntax.lens.* -import com.digitalasset.canton.environment.CantonEnvironment /** Definition of how a environment should be configured and setup. * @param baseConfig @@ -58,20 +57,21 @@ import com.digitalasset.canton.environment.CantonEnvironment * making ports unique or some other specialization for the particular tests you're running) */ final case class EnvironmentDefinition( - override val baseConfig: CantonConfig, - override val testingConfig: TestingConfigInternal = + baseConfig: CantonConfig, + testingConfig: TestingConfigInternal = TestingConfigInternal(warnOnAcsCommitmentDegradation = false, warnOnJwtScopeUsage = false), - override val setups: List[TestConsoleEnvironment => Unit] = Nil, - override val teardown: Unit => Unit = _ => (), - override val configTransforms: Seq[ConfigTransform] = ConfigTransforms.defaults, + setups: List[TestConsoleEnvironment => Unit] = Nil, + teardown: Unit => Unit = _ => (), + configTransforms: Seq[ConfigTransform] = ConfigTransforms.defaults, staticSynchronizerParametersMap: Map[String, StaticSynchronizerParameters] = Map.empty, -) extends BaseEnvironmentDefinition[CantonConfig, CantonEnvironment]( - baseConfig, - testingConfig, - setups, - teardown, - configTransforms, - ) { +) { + + /** Create a canton configuration by applying the configTransforms to the base config. Some + * transforms may have side-effects (such as incrementing the next available port number) so only + * do before constructing an environment. + */ + def generateConfig: CantonConfig = + configTransforms.foldLeft(baseConfig)((config, transform) => transform(config)) def withManualStart: EnvironmentDefinition = copy(baseConfig = baseConfig.focus(_.parameters.manualStart).replace(true)) @@ -107,9 +107,7 @@ final case class EnvironmentDefinition( ) } - def withSetup( - setup: TestConsoleEnvironment => Unit - ): EnvironmentDefinition = + def withSetup(setup: TestConsoleEnvironment => Unit): EnvironmentDefinition = copy(setups = setups :+ setup) def withTeardown(teardown: Unit => Unit): EnvironmentDefinition = @@ -150,13 +148,13 @@ final case class EnvironmentDefinition( copy(staticSynchronizerParametersMap = map) def createTestConsole( - environment: CantonEnvironment, + environment: Environment, loggerFactory: NamedLoggerFactory, ): TestConsoleEnvironment = - new CantonConsoleEnvironment( + new ConsoleEnvironment( environment, new TestConsoleOutput(loggerFactory), - ) with CantonTestEnvironment + ) with TestEnvironment } /** Default testing environments for integration tests @@ -255,9 +253,7 @@ object EnvironmentDefinition extends LazyLogging { lazy val simpleTopology: EnvironmentDefinition = fromResource("examples/01-simple-topology/simple-topology.conf") - def S1M1(implicit - env: TestConsoleEnvironment - ): NetworkTopologyDescription = { + def S1M1(implicit env: TestConsoleEnvironment): NetworkTopologyDescription = { import env.* NetworkTopologyDescription( @@ -271,9 +267,7 @@ object EnvironmentDefinition extends LazyLogging { def S2M1( synchronizerOwnersOverride: Option[Seq[InstanceReference]] = None - )(implicit - env: TestConsoleEnvironment - ): NetworkTopologyDescription = { + )(implicit env: TestConsoleEnvironment): NetworkTopologyDescription = { import env.* NetworkTopologyDescription( @@ -285,9 +279,7 @@ object EnvironmentDefinition extends LazyLogging { ) } - def S2M2(implicit - env: TestConsoleEnvironment - ): NetworkTopologyDescription = { + def S2M2(implicit env: TestConsoleEnvironment): NetworkTopologyDescription = { import env.* NetworkTopologyDescription( @@ -299,9 +291,7 @@ object EnvironmentDefinition extends LazyLogging { ) } - def S4M4(implicit - env: TestConsoleEnvironment - ): NetworkTopologyDescription = { + def S4M4(implicit env: TestConsoleEnvironment): NetworkTopologyDescription = { import env.* NetworkTopologyDescription( @@ -336,9 +326,7 @@ object EnvironmentDefinition extends LazyLogging { ) } - def S1M1_S1M1(implicit - env: TestConsoleEnvironment - ): Seq[NetworkTopologyDescription] = { + def S1M1_S1M1(implicit env: TestConsoleEnvironment): Seq[NetworkTopologyDescription] = { import env.* Seq( @@ -468,7 +456,7 @@ object EnvironmentDefinition extends LazyLogging { numMediators = 2, ) - lazy val P1S1M1_Config = buildBaseEnvironmentDefinition( + lazy val P1S1M1_Config: EnvironmentDefinition = buildBaseEnvironmentDefinition( numParticipants = 1, numSequencers = 1, numMediators = 1, @@ -748,7 +736,7 @@ object EnvironmentDefinition extends LazyLogging { ) } - lazy val P3_S1M1_Config: EnvironmentDefinition = + lazy val P3S1M1_Config: EnvironmentDefinition = buildBaseEnvironmentDefinition( numParticipants = 3, numSequencers = 1, @@ -780,7 +768,7 @@ object EnvironmentDefinition extends LazyLogging { * - 1 synchronizer with 1 sequencer and 1 mediator */ lazy val P3_S1M1: EnvironmentDefinition = - P3_S1M1_Config.withNetworkBootstrap { implicit env => + P3S1M1_Config.withNetworkBootstrap { implicit env => new NetworkBootstrapper(S1M1) } @@ -788,8 +776,8 @@ object EnvironmentDefinition extends LazyLogging { * - 1 synchronizer with 1 sequencer and 1 mediator * - no initialized synchronizer */ - lazy val P3_S1M1_Manual: EnvironmentDefinition = - P3_S1M1_Config.withManualStart + lazy val P3S1M1_Manual: EnvironmentDefinition = + P3S1M1_Config.withManualStart lazy val P3S2M2_Config: EnvironmentDefinition = buildBaseEnvironmentDefinition( @@ -897,9 +885,10 @@ object EnvironmentDefinition extends LazyLogging { } /** - 5 participants '''not''' connected to the synchronizer - * - 2 synchronizers with 1 sequencer and 1 mediator each + * - 2 sequencers + * - 2 mediators */ - lazy val P5_S1M1_S1M1_Config: EnvironmentDefinition = + lazy val P5S2M2_Config: EnvironmentDefinition = buildBaseEnvironmentDefinition( numParticipants = 5, numSequencers = 2, @@ -910,12 +899,12 @@ object EnvironmentDefinition extends LazyLogging { * - 2 synchronizers with 1 sequencer and 1 mediator each */ lazy val P5_S1M1_S1M1: EnvironmentDefinition = - P5_S1M1_S1M1_Config.withNetworkBootstrap { implicit env => + P5S2M2_Config.withNetworkBootstrap { implicit env => NetworkBootstrapper(S1M1_S1M1) } lazy val P5_S1M1_S1M1_Manual: EnvironmentDefinition = - P5_S1M1_S1M1_Config.withManualStart + P5S2M2_Config.withManualStart /** - 5 participants * - 4 sequencers @@ -934,7 +923,7 @@ object EnvironmentDefinition extends LazyLogging { * - 1 mediators * - no initialized synchronizer */ - lazy val P1_S2M1_Manual: EnvironmentDefinition = + lazy val P1S2M1_Manual: EnvironmentDefinition = buildBaseEnvironmentDefinition( numParticipants = 1, numSequencers = 2, @@ -979,16 +968,7 @@ object EnvironmentDefinition extends LazyLogging { /** - 3 participants '''not''' connected to any synchronizer * - 2 synchronizers with 1 sequencer and 1 mediator each */ - lazy val P3_S1M1_S1M1_Config: EnvironmentDefinition = buildBaseEnvironmentDefinition( - numParticipants = 3, - numSequencers = 2, - numMediators = 2, - ) - - /** - 3 participants '''not''' connected to any synchronizer - * - 2 synchronizers with 1 sequencer and 1 mediator each - */ - lazy val P3_S1M1_S1M1: EnvironmentDefinition = P3_S1M1_S1M1_Config + lazy val P3_S1M1_S1M1: EnvironmentDefinition = P3S2M2_Config .withNetworkBootstrap { implicit env => NetworkBootstrapper(S1M1_S1M1) } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala index 33fe1080c1..ebb7ecc0af 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetup.scala @@ -1,65 +1,59 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 package com.digitalasset.canton.integration import com.digitalasset.canton.CloseableTest -import com.digitalasset.canton.admin.api.client.commands.{ - GrpcAdminCommand, - ParticipantAdminCommands, -} -import com.digitalasset.canton.admin.api.client.commands.LedgerApiCommands.{ - CommandService, - CommandSubmissionService, -} -import com.digitalasset.canton.config.{DefaultPorts, SharedCantonConfig, TestingConfigInternal} import com.digitalasset.canton.config.RequireTypes.PositiveInt -import com.digitalasset.canton.environment.{Environment, EnvironmentFactory} -import com.digitalasset.canton.error.TransactionRoutingError -import com.digitalasset.canton.integration.EnvironmentSetup.EnvironmentSetupException +import com.digitalasset.canton.config.{CantonConfig, DefaultPorts, TestingConfigInternal} +import com.digitalasset.canton.environment.EnvironmentFactory import com.digitalasset.canton.integration.plugins.{UseH2, UsePostgres, UseReferenceBlockSequencer} import com.digitalasset.canton.logging.{LogEntry, NamedLogging, SuppressingLogger} import com.digitalasset.canton.metrics.{MetricsFactoryType, ScopedInMemoryMetricsFactory} -import com.digitalasset.canton.networking.grpc.GrpcError -import com.digitalasset.canton.participant.sync.SyncServiceInjectionError -import com.digitalasset.canton.tracing.TraceContext import org.scalatest.{Assertion, BeforeAndAfterAll, Suite} -import scala.util.Try -import scala.util.control.{NonFatal, NoStackTrace} +import scala.util.control.NonFatal /** Provides an ability to create a canton environment when needed for test. Include * [[IsolatedEnvironments]] or [[SharedEnvironment]] to determine when this happens. Uses * [[ConcurrentEnvironmentLimiter]] to ensure we limit the number of concurrent environments in a * test run. */ -sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] - extends BeforeAndAfterAll { +sealed trait EnvironmentSetup extends BeforeAndAfterAll { this: Suite with NamedLogging => - protected def environmentDefinition: BaseEnvironmentDefinition[C, E] + protected def environmentDefinition: EnvironmentDefinition - private lazy val envDef: BaseEnvironmentDefinition[C, E] = environmentDefinition + private lazy val envDef: EnvironmentDefinition = environmentDefinition - protected def environmentFactory: EnvironmentFactory[C, E] + protected def environmentFactory: EnvironmentFactory // plugins are registered during construction from a single thread @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var plugins: Seq[BaseEnvironmentSetupPlugin[C, E]] = Seq() + private var plugins: Seq[EnvironmentSetupPlugin] = Seq() - protected[integration] def registerPlugin(plugin: BaseEnvironmentSetupPlugin[C, E]): Unit = + protected[integration] def registerPlugin(plugin: EnvironmentSetupPlugin): Unit = plugins = plugins :+ plugin + override protected def beforeAll(): Unit = { + plugins.foreach(_.beforeTests()) + super.beforeAll() + } + + override protected def afterAll(): Unit = + try super.afterAll() + finally plugins.foreach(_.afterTests()) + /** Provide an environment for an individual test either by reusing an existing one or creating a * new one depending on the approach being used. */ - def provideEnvironment(testName: String): BaseTestConsoleEnvironment[C, E] + def provideEnvironment: TestConsoleEnvironment /** Optional hook for implementors to know when a test has finished and be provided the * environment instance. This is required over a afterEach hook as we need the environment * instance passed. */ - def testFinished(testName: String, environment: BaseTestConsoleEnvironment[C, E]): Unit = {} + def testFinished(environment: TestConsoleEnvironment): Unit = {} override val loggerFactory: SuppressingLogger = SuppressingLogger(getClass) @@ -86,157 +80,80 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] * a new test console environment */ protected def manualCreateEnvironment( - initialConfig: C = envDef.generateConfig, - configTransform: C => C = identity, - runPlugins: BaseEnvironmentSetupPlugin[C, E] => Boolean = _ => true, + initialConfig: CantonConfig = envDef.generateConfig, + configTransform: ConfigTransform = identity, + runPlugins: EnvironmentSetupPlugin => Boolean = _ => true, testConfigTransform: TestingConfigInternal => TestingConfigInternal = identity, - testName: Option[String], - ): BaseTestConsoleEnvironment[C, E] = - TraceContext.withNewTraceContext("manualCreateEnvironment") { tc => - logger.debug( - s"Starting creating environment for $suiteName${testName.fold("")(n => s", test '$n'")}:" - )(tc) - import org.scalactic.source - def step[T](name: String)(expr: => T)(implicit pos: source.Position): T = { - logger.debug(s"Starting creating environment: $name")(tc) - Try(expr) match { - case scala.util.Success(value) => - logger.debug(s"Finished creating environment: $name")(tc) - value - case scala.util.Failure(exception) => - val loc = s"(${pos.fileName}:${pos.lineNumber})" - logger.error(s"Failed creating environment: $name $loc", exception)(tc) - throw EnvironmentSetupException( - s"Creating environment failed at step $name $loc", - exception, - ) - } - } - val testConfig = initialConfig - // note: beforeEnvironmentCreate may well have side-effects (e.g. starting databases or docker containers) - val pluginConfig = step("Running plugins before") { - plugins.foldLeft(testConfig)((config, plugin) => - if (runPlugins(plugin)) - plugin.beforeEnvironmentCreated(config) - else - config - ) - } - - // Once all the plugins and config transformation is done apply the defaults - val finalConfig = - configTransform(pluginConfig).withDefaults(Some(DefaultPorts.create())) - - val scopedMetricsFactory = new ScopedInMemoryMetricsFactory - val environmentFixture: E = - step("Creating fixture") { - environmentFactory.create( - finalConfig, - loggerFactory, - testConfigTransform( - envDef.testingConfig.copy( - metricsFactoryType = - /* If metrics reporters were configured for the test then it's an externally observed test - * therefore actual metrics have to be reported. - * The in memory metrics are used when no reporters are configured and the metrics are - * observed directly in the test scenarios. - * - * In this case, you can grab the metrics from the [[MetricsRegistry.generateMetricsFactory]] method, - * which is accessible using env.environment.metricsRegistry - * - * */ - if (finalConfig.monitoring.metrics.reporters.isEmpty) - MetricsFactoryType.InMemory(scopedMetricsFactory) - else MetricsFactoryType.External, - initializeGlobalOpenTelemetry = false, - sequencerTransportSeed = Some(1L), - ) - ), + ): TestConsoleEnvironment = { + val testConfig = initialConfig + // note: beforeEnvironmentCreate may well have side-effects (e.g. starting databases or docker containers) + val pluginConfig = + plugins.foldLeft(testConfig)((config, plugin) => + if (runPlugins(plugin)) + plugin.beforeEnvironmentCreated(config) + else + config + ) + + // Once all the plugins and config transformation is done apply the defaults + val finalConfig = + configTransform(pluginConfig).withDefaults(Some(DefaultPorts.create())) + + val scopedMetricsFactory = new ScopedInMemoryMetricsFactory + val environmentFixture = + environmentFactory.create( + finalConfig, + loggerFactory, + testConfigTransform( + envDef.testingConfig.copy( + metricsFactoryType = + /* If metrics reporters were configured for the test then it's an externally observed test + * therefore actual metrics have to be reported. + * The in memory metrics are used when no reporters are configured and the metrics are + * observed directly in the test scenarios. + * + * In this case, you can grab the metrics from the [[MetricsRegistry.generateMetricsFactory]] method, + * which is accessible using env.environment.metricsRegistry + * + * */ + if (finalConfig.monitoring.metrics.reporters.isEmpty) + MetricsFactoryType.InMemory(scopedMetricsFactory) + else MetricsFactoryType.External, + initializeGlobalOpenTelemetry = false, + sequencerTransportSeed = Some(1L), ) - } - - try { - val testEnvironment: BaseTestConsoleEnvironment[C, E] = step("Creating test console") { - envDef.createTestConsole(environmentFixture, loggerFactory) - } - - // In tests, we want to retry some commands to avoid flakiness - def commandRetryPolicy(command: GrpcAdminCommand[?, ?, ?])(error: GrpcError): Boolean = { - val decodedCantonError = command match { - // Command submissions are safe to retry - they are deduplicated by command ID - case _: CommandSubmissionService.BaseCommand[?, ?, ?] => error.decodedCantonError - case _: CommandService.BaseCommand[?, ?, ?] => error.decodedCantonError - // Package operations are idempotent so we can retry them - case _: ParticipantAdminCommands.Package.PackageCommand[?, ?, ?] => - error.decodedCantonError - // Other commands might not be idempotent - case _ => None - } - // Ideally we would reuse the logic from RetryProvider.RetryableError but that produces a circular dependency - // so for now we go for an ad-hoc logic here. - val shouldRetry = decodedCantonError.exists { err => - val retryableErrorCodes = Seq( - // Normally "not connected to synchronizer" errors are not retriable because they require operator intervention. - // In canton network, synchronizer connections are managed by the sv app automation - // and transient disconnects are normal. - SyncServiceInjectionError.NotConnectedToAnySynchronizer, - SyncServiceInjectionError.NotConnectedToSynchronizer, - TransactionRoutingError.TopologyErrors.UnknownContractSynchronizers, - TransactionRoutingError.TopologyErrors.UnknownInformees, - ) - val forceRetry = retryableErrorCodes.exists(retryableErroCode => - err.code == retryableErroCode.code || err.code.id == retryableErroCode.id // some errors are wrapped in a GenericErrorCode so they lose the type information - ) - if (forceRetry) { - true - } else { - err.isRetryable - } - } - - logger.debug( - s"Got canton error $decodedCantonError from command $command. Retrying: $shouldRetry" - )(tc) - - shouldRetry - } - - step("Updating retry policy") { - testEnvironment.grpcLedgerCommandRunner.setRetryPolicy(commandRetryPolicy) - testEnvironment.grpcAdminCommandRunner.setRetryPolicy(commandRetryPolicy) - } + ), + ) - step("Running plugins after") { - plugins.foreach(plugin => - if (runPlugins(plugin)) plugin.afterEnvironmentCreated(finalConfig, testEnvironment) - ) - } - - if (!finalConfig.parameters.manualStart) - step("Starting all nodes") { - handleStartupLogs(testEnvironment.startAll()) - } - - step("Running setup") { - envDef.setups.foreach(setup => setup(testEnvironment)) + try { + val testEnvironment: TestConsoleEnvironment = + envDef.createTestConsole(environmentFixture, loggerFactory) + + plugins.foreach(plugin => + if (runPlugins(plugin)) plugin.afterEnvironmentCreated(finalConfig, testEnvironment) + ) + + if (!finalConfig.parameters.manualStart) + handleStartupLogs(testEnvironment.startAll()) + + envDef.setups.foreach(setup => setup(testEnvironment)) + + testEnvironment + } catch { + case NonFatal(ex) => + // attempt to ensure the environment is shutdown if anything in the startup of initialization fails + try { + environmentFixture.close() + } catch { + case NonFatal(shutdownException) => + // we suppress the exception thrown by the shutdown as we want to propagate the original + // exception, however add it to the suppressed list on this thrown exception + ex.addSuppressed(shutdownException) } - - testEnvironment - } catch { - case NonFatal(ex) => - // attempt to ensure the environment is shutdown if anything in the startup of initialization fails - try { - environmentFixture.close() - } catch { - case NonFatal(shutdownException) => - // we suppress the exception thrown by the shutdown as we want to propagate the original - // exception, however add it to the suppressed list on this thrown exception - ex.addSuppressed(shutdownException) - } - // rethrow exception - throw ex - } + // rethrow exception + throw ex } + } /** Creates a new environment manually for a test with the storage plugins disabled, so that we * can keep the persistent state from an old environment. @@ -253,11 +170,10 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] * a new test console environment that persists the db/state of another 'older' environment */ protected def manualCreateEnvironmentWithPreviousState( - oldEnvConfig: C, - configTransform: C => C = identity, - runPlugins: BaseEnvironmentSetupPlugin[C, E] => Boolean = _ => true, - testName: Option[String], - ): BaseTestConsoleEnvironment[C, E] = + oldEnvConfig: CantonConfig, + configTransform: ConfigTransform = identity, + runPlugins: EnvironmentSetupPlugin => Boolean = _ => true, + ): TestConsoleEnvironment = manualCreateEnvironment( oldEnvConfig, configTransform, @@ -269,16 +185,14 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] false // to prevent creating a new fresh db, the db is only deleted when the old environment is destroyed. case plugin => runPlugins(plugin) }, - testName = testName, ) - protected def createEnvironment(testName: Option[String]): BaseTestConsoleEnvironment[C, E] = { - ConcurrentEnvironmentLimiter.create(getClass.getName, numPermits) { - manualCreateEnvironment(testName = testName) - } - } + protected def createEnvironment(): TestConsoleEnvironment = + ConcurrentEnvironmentLimiter.create(getClass.getName, numPermits)( + manualCreateEnvironment() + ) - protected def manualDestroyEnvironment(environment: BaseTestConsoleEnvironment[C, E]): Unit = { + protected def manualDestroyEnvironment(environment: TestConsoleEnvironment): Unit = { val config = environment.actualConfig plugins.foreach(_.beforeEnvironmentDestroyed(environment)) try { @@ -289,10 +203,8 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] } } - protected def destroyEnvironment( - testName: Option[String], - environment: BaseTestConsoleEnvironment[C, E], - ): Unit = { + protected def destroyEnvironment(environment: TestConsoleEnvironment): Unit = { + // Run the Ledger API integrity check before destroying the environment val checker = new LedgerApiStoreIntegrityChecker(loggerFactory) checker.verifyParticipantLapiIntegrity(environment, plugins) @@ -309,37 +221,29 @@ sealed trait EnvironmentSetup[C <: SharedCantonConfig[C], E <: Environment[C]] protected def numPermits: PositiveInt = PositiveInt.one } -object EnvironmentSetup { - final case class EnvironmentSetupException(message: String, cause: Throwable) - extends RuntimeException(message, cause) - with NoStackTrace // Stack trace is just a lot of scalatest and EnvironmentSetup -} - /** Starts an environment in a beforeAll test and uses it for all tests. Destroys it in an afterAll * hook. * * As a result, the environment state at the beginning of a test case equals the state at the end * of the previous test case. */ -trait BaseSharedEnvironment[C <: SharedCantonConfig[C], E <: Environment[C]] - extends EnvironmentSetup[C, E] - with CloseableTest { +trait SharedEnvironment extends EnvironmentSetup with CloseableTest { this: Suite with NamedLogging => @SuppressWarnings(Array("org.wartremover.warts.Var")) - private var sharedEnvironment: Option[BaseTestConsoleEnvironment[C, E]] = None + private var sharedEnvironment: Option[TestConsoleEnvironment] = None override protected def beforeAll(): Unit = { super.beforeAll() - sharedEnvironment = Some(createEnvironment(None)) + sharedEnvironment = Some(createEnvironment()) } override def afterAll(): Unit = try { - sharedEnvironment.foreach(destroyEnvironment(None, _)) + sharedEnvironment.foreach(destroyEnvironment) } finally super.afterAll() - override def provideEnvironment(testName: String): BaseTestConsoleEnvironment[C, E] = + override def provideEnvironment: TestConsoleEnvironment = sharedEnvironment.getOrElse( sys.error("beforeAll should have run before providing a shared environment") ) @@ -351,15 +255,11 @@ trait BaseSharedEnvironment[C <: SharedCantonConfig[C], E <: Environment[C]] * Try to use SharedEnvironment instead to avoid the cost of frequently creating environments in * CI. */ -trait BaseIsolatedEnvironments[C <: SharedCantonConfig[C], E <: Environment[C]] - extends EnvironmentSetup[C, E] { +trait IsolatedEnvironments extends EnvironmentSetup { this: Suite with NamedLogging => - override def provideEnvironment(testName: String): BaseTestConsoleEnvironment[C, E] = - createEnvironment(Some(testName)) - override def testFinished(testName: String, environment: BaseTestConsoleEnvironment[C, E]): Unit = - destroyEnvironment( - Some(testName), - environment, - ) + override def provideEnvironment: TestConsoleEnvironment = createEnvironment() + override def testFinished(environment: TestConsoleEnvironment): Unit = destroyEnvironment( + environment + ) } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetupPlugin.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetupPlugin.scala index 21773c01c5..b40e75287d 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetupPlugin.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/EnvironmentSetupPlugin.scala @@ -3,8 +3,7 @@ package com.digitalasset.canton.integration -import com.digitalasset.canton.config.SharedCantonConfig -import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.config.CantonConfig import com.digitalasset.canton.logging.NamedLogging /** Plugin to allow multiple tests to reuse behavior modifying the test config or environment @@ -14,8 +13,7 @@ import com.digitalasset.canton.logging.NamedLogging * [[SharedEnvironment]] integration test setups. Must call [[EnvironmentSetup.registerPlugin]] * within its constructor to register the plugin. */ -trait BaseEnvironmentSetupPlugin[C <: SharedCantonConfig[C], E <: Environment[C]] - extends NamedLogging { +trait EnvironmentSetupPlugin extends NamedLogging { /** Run before any of the tests in the test class have been run or an environment created */ def beforeTests(): Unit = {} @@ -27,17 +25,17 @@ trait BaseEnvironmentSetupPlugin[C <: SharedCantonConfig[C], E <: Environment[C] /** Hook before the environment has been created. Returned config will be used for the test. */ - def beforeEnvironmentCreated(config: C): C = config + def beforeEnvironmentCreated(config: CantonConfig): CantonConfig = config /** Hook after the environment has been created but no tests have yet been run */ - def afterEnvironmentCreated(config: C, environment: BaseTestConsoleEnvironment[C, E]): Unit = {} + def afterEnvironmentCreated(config: CantonConfig, environment: TestConsoleEnvironment): Unit = {} /** Hook after all tests from the test class have completed but the environment is still running */ def beforeEnvironmentDestroyed( - environment: BaseTestConsoleEnvironment[C, E] + environment: TestConsoleEnvironment ): Unit = {} /** Hook after the tests have been run and the environment has been shutdown */ - def afterEnvironmentDestroyed(config: C): Unit = {} + def afterEnvironmentDestroyed(config: CantonConfig): Unit = {} } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/LedgerApiStoreIntegrityChecker.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/LedgerApiStoreIntegrityChecker.scala index e83ff8a80c..d5e9f8ad7a 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/LedgerApiStoreIntegrityChecker.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/LedgerApiStoreIntegrityChecker.scala @@ -4,8 +4,7 @@ package com.digitalasset.canton.integration import com.digitalasset.canton.LedgerParticipantId -import com.digitalasset.canton.config.{SharedCantonConfig, StorageConfig} -import com.digitalasset.canton.config.CantonRequireTypes.InstanceName +import com.digitalasset.canton.config.StorageConfig import com.digitalasset.canton.console.FeatureFlag import com.digitalasset.canton.integration.plugins.UseExternalProcess import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} @@ -25,8 +24,8 @@ class LedgerApiStoreIntegrityChecker( with NoTracing { def verifyParticipantLapiIntegrity( - env: AnyTestConsoleEnvironment, - plugins: Seq[BaseEnvironmentSetupPlugin[? <: SharedCantonConfig[?], ?]], + env: TestConsoleEnvironment, + plugins: Seq[EnvironmentSetupPlugin], ): Unit = { import env.* @@ -157,7 +156,7 @@ class LedgerApiStoreIntegrityChecker( participantLoggingName = s"not running local participant ${notRunningParticipant.name}", storageConfig = actualConfig.participants .getOrElse( - InstanceName.tryCreate(notRunningParticipant.name), + notRunningParticipant.name, throw new IllegalStateException( s"No configuration found for a not running participant $notRunningParticipant." ), diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala index 72fc407815..c6f3a788d2 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/TestEnvironment.scala @@ -15,9 +15,11 @@ import com.digitalasset.canton.concurrent.{ import com.digitalasset.canton.config.{ BatchingConfig, CachingConfigs, + CantonConfig, CryptoConfig, SessionEncryptionKeyCacheConfig, } +import com.digitalasset.canton.console.commands.GlobalSecretKeyAdministration import com.digitalasset.canton.console.{ ConsoleEnvironment, ConsoleEnvironmentTestHelpers, @@ -25,7 +27,6 @@ import com.digitalasset.canton.console.{ InstanceReference, LocalInstanceReference, } -import com.digitalasset.canton.console.commands.GlobalSecretKeyAdministration import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.integration.bootstrap.InitializedSynchronizer import com.digitalasset.canton.lifecycle.FutureUnlessShutdown @@ -39,21 +40,21 @@ import scala.collection.mutable import scala.concurrent.Await /** Type including all environment macros and utilities to appear as you're using canton console */ -trait TestEnvironment[+C] +trait TestEnvironment extends ConsoleEnvironmentTestHelpers with ConsoleMacros with ConsoleEnvironment.Implicits with EnvironmentTestHelpers with CommonTestAliases { - this: ConsoleEnvironment { type Config <: C } => + this: ConsoleEnvironment => implicit val executionContext: ExecutionContextIdlenessExecutorService = environment.executionContext - implicit def actorSystem: ActorSystem = environment.actorSystem + implicit val actorSystem: ActorSystem = environment.actorSystem implicit val executionSequencerFactory: ExecutionSequencerFactory = environment.executionSequencerFactory - def actualConfig: C = environment.config + def actualConfig: CantonConfig = environment.config private lazy val storage = new MemoryStorage(loggerFactory, environmentTimeouts) diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala index 61f560198e..6aa8fb4b67 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/bootstrap/NetworkBootstrapper.scala @@ -120,9 +120,7 @@ object NetworkTopologyDescription { ] = None, overrideStaticSynchronizerParameters: Option[StaticSynchronizerParameters] = None, mediatorThreshold: PositiveInt = PositiveInt.one, - )(implicit - env: TestConsoleEnvironment - ): NetworkTopologyDescription = + )(implicit env: TestConsoleEnvironment): NetworkTopologyDescription = NetworkTopologyDescription( synchronizerName = synchronizerAlias.unwrap, synchronizerOwners, diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/package.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/package.scala index ffb90c036b..bbca2c124a 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/package.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/package.scala @@ -3,22 +3,12 @@ package com.digitalasset.canton -import com.digitalasset.canton.config.{CantonConfig, SharedCantonConfig, StorageConfig} -import com.digitalasset.canton.environment.{CantonEnvironment, Environment} +import com.digitalasset.canton.config.{CantonConfig, StorageConfig} +import com.digitalasset.canton.console.ConsoleEnvironment import com.digitalasset.canton.integration.ConfigTransforms.ConfigNodeType package object integration { - type BaseTestConsoleEnvironment[C <: SharedCantonConfig[C], +E <: Environment[C]] = - E#Console & TestEnvironment[? <: C] - type AnyTestConsoleEnvironment = - BaseTestConsoleEnvironment[? <: SharedCantonConfig[?], ? <: Environment[?]] - type TestConsoleEnvironment = BaseTestConsoleEnvironment[CantonConfig, CantonEnvironment] - type CantonTestEnvironment = TestEnvironment[CantonConfig] - type EnvironmentSetupPlugin = BaseEnvironmentSetupPlugin[CantonConfig, CantonEnvironment] - type CantonEnvironmentSetup = EnvironmentSetup[CantonConfig, CantonEnvironment] - type SharedEnvironment = BaseSharedEnvironment[CantonConfig, CantonEnvironment] - type IsolatedEnvironments = BaseIsolatedEnvironments[CantonConfig, CantonEnvironment] - type CantonBaseIntegrationTest = BaseIntegrationTest[CantonConfig, CantonEnvironment] + type TestConsoleEnvironment = ConsoleEnvironment & TestEnvironment type ConfigTransform = CantonConfig => CantonConfig type StorageConfigTransform = (ConfigNodeType, String, StorageConfig) => StorageConfig diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseBftSequencer.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseBftSequencer.scala index a283830b25..adeb917638 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseBftSequencer.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseBftSequencer.scala @@ -80,7 +80,6 @@ final class UseBftSequencer( maxBatchesPerBlockProposal: Short = DefaultMaxBatchesPerProposal, availabilityMinProposalCreationDelay: FiniteDuration = 50.millis, dedicatedExecutionContextDivisor: Option[Int] = DefaultDedicatedExecutionContextDivisor, - availabilityMaxProposalCreationDelay: FiniteDuration = 50.millis, ) extends EnvironmentSetupPlugin { private val tmpDir = better.files.File(System.getProperty("java.io.tmpdir")) diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseOtlp.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseOtlp.scala index 8c7070ffac..2979c52a52 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseOtlp.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseOtlp.scala @@ -64,7 +64,7 @@ class OtlpGrpcServer(protected val loggerFactory: NamedLoggerFactory) lock.exclusive( traceSpans.addAll( request.getResourceSpansList.asScala - .flatMap(_.getInstrumentationLibrarySpansList.asScala) + .flatMap(_.getScopeSpansList.asScala) .flatMap(_.getSpansList.asScala) ) ) diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseReferenceBlockSequencer.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseReferenceBlockSequencer.scala index 3330a67c3d..87c91242c9 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseReferenceBlockSequencer.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseReferenceBlockSequencer.scala @@ -74,7 +74,7 @@ class UseReferenceBlockSequencer[StorageConfigT <: StorageConfig]( NonEmpty( List, dbNameForGroup(0), // db 0 is the default one - (1 to sequencerGroups.numberOfSynchronizers).map(i => dbNameForGroup(i)).toList* + (1 to sequencerGroups.numberOfSynchronizers).map(i => dbNameForGroup(i)).toList *, ) } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/tinyproxy/UseTinyProxy.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/tinyproxy/UseTinyProxy.scala index cdc7477038..762b9f2db0 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/tinyproxy/UseTinyProxy.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/tinyproxy/UseTinyProxy.scala @@ -104,9 +104,7 @@ final case class UseTinyProxy(tinyProxyConfig: TinyProxyConfig) backgroundProcessHandler.killAndRemove() } - override def beforeEnvironmentDestroyed( - environment: TestConsoleEnvironment - ): Unit = + override def beforeEnvironmentDestroyed(environment: TestConsoleEnvironment): Unit = externalPlugin.beforeEnvironmentDestroyed(environment) } diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/AcsInspection.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/AcsInspection.scala index 06a174d061..0f78f1c235 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/AcsInspection.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/AcsInspection.scala @@ -10,8 +10,9 @@ import com.daml.ledger.api.v2.state_service.IncompleteUnassigned import com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedContractEntry import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.{LocalParticipantReference, ParticipantReference} +import com.digitalasset.canton.examples.java as M import com.digitalasset.canton.protocol.{ContractInstance, LfContractId} -import com.digitalasset.canton.topology.{PartyId, SynchronizerId} +import com.digitalasset.canton.topology.{Party, PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.{BaseTest, SynchronizerAlias} import com.digitalasset.daml.lf.data.Ref.PackageId @@ -277,6 +278,25 @@ trait AcsInspection { PackageId.assertFromString(resultAsString) } + def findIOU( + participant: LocalParticipantReference, + obligor: Party, + owner: Party, + ): M.iou.Iou.Contract = + participant.ledger_api.javaapi.state.acs + .await(M.iou.Iou.COMPANION)( + obligor, + contract => + contract.data.owner == owner.toProtoPrimitive && contract.data.payer == obligor.toProtoPrimitive, + ) + + def findIOU( + participant: LocalParticipantReference, + submitter: Party, + predicate: M.iou.Iou.Contract => Boolean, + ): M.iou.Iou.Contract = + participant.ledger_api.javaapi.state.acs.await(M.iou.Iou.COMPANION)(submitter, predicate) + private def comparableCreatedEvent(createdEvent: Option[CreatedEvent]): Option[CreatedEvent] = createdEvent.map(_.copy(offset = 0L, nodeId = 0)) diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/EntitySyntax.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/EntitySyntax.scala index 7a4e1c0051..033ca5c822 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/EntitySyntax.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/util/EntitySyntax.scala @@ -58,9 +58,7 @@ trait EntitySyntax { } } - implicit class PartyIdSyntax(partyId: PartyId)(implicit - env: TestConsoleEnvironment - ) { + implicit class PartyIdSyntax(partyId: PartyId)(implicit env: TestConsoleEnvironment) { def participants( requestingParticipant: LocalParticipantReference, synchronizerId: Option[SynchronizerId] = None, @@ -71,9 +69,7 @@ trait EntitySyntax { .toSet } - implicit class StringConversions(name: String)(implicit - env: TestConsoleEnvironment - ) { + implicit class StringConversions(name: String)(implicit env: TestConsoleEnvironment) { import env.* def toPartyId( diff --git a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/ProgrammableSequencerPolicies.scala b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/ProgrammableSequencerPolicies.scala index 153c21958f..274252f865 100644 --- a/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/ProgrammableSequencerPolicies.scala +++ b/canton/community/integration-testing/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/ProgrammableSequencerPolicies.scala @@ -26,7 +26,7 @@ object ProgrammableSequencerPolicies { /* Delay the specified messages by some duration. */ - def delay(environment: Environment[?])( + def delay(environment: Environment)( confirmationResponses: Map[ParticipantId, NonNegativeFiniteDuration] = Map.empty, mediatorMessages: Option[NonNegativeFiniteDuration] = None, ): SendPolicy = { diff --git a/canton/community/ledger-test-tool/tool/src/main/resources/logback.xml b/canton/community/ledger-test-tool/tool/src/main/resources/logback.xml deleted file mode 100644 index 03a157a14b..0000000000 --- a/canton/community/ledger-test-tool/tool/src/main/resources/logback.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - System.err - - trace - - - %date [%thread] %-5level %logger{10} - %msg%n - - - - - - - - - - - - - - diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala index 809109255d..e328150db9 100644 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala +++ b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/RequiredClaims.scala @@ -16,18 +16,18 @@ object RequiredClaims { readAs: Set[String], userIdL: Lens[Req, String], ): List[RequiredClaim[Req]] = - RequiredClaim.MatchUserId(userIdL) :: - actAs.view.map(RequiredClaim.ActAs[Req]).toList ::: - readAs.view.map(RequiredClaim.ReadAs[Req]).toList + RequiredClaim.MatchUserId(userIdL) + :: actAs.view.map(RequiredClaim.ActAs[Req]).toList + ::: readAs.view.map(RequiredClaim.ReadAs[Req]).toList def executionClaims[Req]( executeAs: Set[String], readAs: Set[String], userIdL: Lens[Req, String], ): List[RequiredClaim[Req]] = - RequiredClaim.MatchUserId(userIdL) :: - executeAs.view.map(RequiredClaim.ExecuteAs[Req]).toList ::: - readAs.view.map(RequiredClaim.ReadAs[Req]).toList + RequiredClaim.MatchUserId(userIdL) + :: executeAs.view.map(RequiredClaim.ExecuteAs[Req]).toList + ::: readAs.view.map(RequiredClaim.ReadAs[Req]).toList def readAsForAllParties[Req](parties: Iterable[String]): List[RequiredClaim[Req]] = parties.view.map(RequiredClaim.ReadAs[Req]).toList @@ -36,8 +36,8 @@ object RequiredClaims { transactionFormat.eventFormat.toList.flatMap(RequiredClaims.eventFormatClaims[Req]) def eventFormatClaims[Req](eventFormat: EventFormat): List[RequiredClaim[Req]] = - readAsForAllParties[Req](eventFormat.filtersByParty.keys) ::: - eventFormat.filtersForAnyParty.map(_ => RequiredClaim.ReadAsAnyParty[Req]()).toList + readAsForAllParties[Req](eventFormat.filtersByParty.keys) + ::: eventFormat.filtersForAnyParty.map(_ => RequiredClaim.ReadAsAnyParty[Req]()).toList def updateFormatClaims[Req](updateFormat: UpdateFormat): List[RequiredClaim[Req]] = List( diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala index 4ce07c2916..d3069b3233 100644 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala +++ b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/client/LedgerClient.scala @@ -151,7 +151,7 @@ object LedgerClient { token .fold(stub)(authorizingStub(stub, _)) .withInterceptors(TraceContextGrpc.clientInterceptor()) - .withOption(TraceContextGrpc.TraceContextCallOptionKey, traceContext) + .withOption(TraceContextGrpc.TraceContextOptionsKey, traceContext) /** A convenient shortcut to build a [[LedgerClient]], use [[fromBuilder]] for a more flexible * alternative. diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala index bb767ff280..4cb7cc4d25 100644 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala +++ b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/error/groups/CommandExecutionErrors.scala @@ -932,12 +932,13 @@ object CommandExecutionErrors extends CommandExecutionErrorGroup { (ErrorResource.TemplateId, err.dstTemplateId.toString), (ErrorResource.PackageName, err.srcPackageName), (ErrorResource.PackageName, err.dstPackageName), - ) ++ encodeParties(err.originalSignatories) ++ - encodeParties(err.originalObservers) ++ - optKeyResources(err.originalKeyOpt) ++ - encodeParties(err.recomputedSignatories) ++ - encodeParties(err.recomputedObservers) ++ - optKeyResources(err.recomputedKeyOpt) + ) + ++ encodeParties(err.originalSignatories) + ++ encodeParties(err.originalObservers) + ++ optKeyResources(err.originalKeyOpt) + ++ encodeParties(err.recomputedSignatories) + ++ encodeParties(err.recomputedObservers) + ++ optKeyResources(err.recomputedKeyOpt) } } } diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala index 1348ff5f28..64fcb9c4cc 100644 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala +++ b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/execution/StoreBackedCommandInterpreter.scala @@ -336,25 +336,11 @@ final class StoreBackedCommandInterpreter( case ResultNeedKey(key, limit, continuationToken, resume) => disclosedOrStoreNKeyLookup(key, limit, continuationToken) - .flatMap { case (fcis, token) => - val entries: Vector[ResultNeedKey.Response.ContractEntry] = fcis.map { fci => - CantonContractIdVersion.extractCantonContractIdVersion(fci.contractId) match { - case Right(version) => - ResultNeedKey.Response.AuthenticableFatContractInstance( - fci, - version.contractHashingMethod, - hash => contractAuthenticator(fci, hash).isRight, - ) - case Left(_) => - ResultNeedKey.Response.UnsupportedContractIdVersion(fci.contractId) - } - } + .flatMap { case (cids, token) => resolveStep( Tracked.value( metrics.execution.engineRunning, - trackSyncExecution(interpretationTimeNanos)( - resume(ResultNeedKey.Response(entries, token)) - ), + trackSyncExecution(interpretationTimeNanos)(resume(cids, token)), ) ) } diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala index 5ed31d6137..78b4c058ae 100644 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala +++ b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/ratelimiting/MemoryCheck.scala @@ -6,13 +6,16 @@ package com.digitalasset.canton.platform.apiserver.ratelimiting import com.digitalasset.canton.ledger.error.LedgerApiErrors.HeapMemoryOverLimit import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory} import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult -import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult.{LimitResultCheck, OverLimit, UnderLimit} +import com.digitalasset.canton.networking.grpc.ratelimiting.LimitResult.{ + LimitResultCheck, + OverLimit, + UnderLimit, +} import com.digitalasset.canton.platform.apiserver.configuration.RateLimitingConfig import java.lang.management.{MemoryMXBean, MemoryPoolMXBean, MemoryType, MemoryUsage} import java.util.concurrent.atomic.AtomicLong import javax.management.ObjectName -import scala.annotation.nowarn import scala.concurrent.duration.{Duration, DurationInt} object MemoryCheck { @@ -100,7 +103,6 @@ object MemoryCheck { } // Delegated methods - @nowarn("cat=deprecation") override def getObjectPendingFinalizationCount: Int = delegate.getObjectPendingFinalizationCount override def getHeapMemoryUsage: MemoryUsage = delegate.getHeapMemoryUsage override def getNonHeapMemoryUsage: MemoryUsage = delegate.getNonHeapMemoryUsage diff --git a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala index f64e357905..6e9b8144e8 100644 --- a/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala +++ b/canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/index/IndexServiceImpl.scala @@ -1256,11 +1256,11 @@ object IndexServiceImpl { lazy val trimmedTransactions = transactions.dropWhile(r => pruningOffset >= updatesToOffset(r)) val fetchedEnoughForFirstPage = - // Trivial exclusion -- no overlap with pruning calculatedEndInclusive.exists( _ < pruningOffset - // Fetched up to calculatedEndInclusive -- if page is shorter it's the last page, so OK. - ) || transactions.sizeIs < limit || trimmedTransactions.lengthIs >= getUpdatesPageRequest.maxPageSize + 1 // Non-trivial case where we need to look into transactions + ) // Trivial exclusion -- no overlap with pruning + || transactions.sizeIs < limit // Fetched up to calculatedEndInclusive -- if page is shorter it's the last page, so OK. + || trimmedTransactions.lengthIs >= getUpdatesPageRequest.maxPageSize + 1 // Non-trivial case where we need to look into transactions if (fetchedEnoughForFirstPage) { buildAscendingPage( diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala index e56cb45244..988f8f728d 100644 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala +++ b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/api/IdentityProviderIdSpec.scala @@ -3,7 +3,7 @@ package com.digitalasset.canton.ledger.api - import com.digitalasset.daml.lf.data.Ref.LedgerString +import com.digitalasset.daml.lf.data.Ref.LedgerString import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala index 0a87a2f5fb..292157fde5 100644 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala +++ b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/PartyRecordStoreTests.scala @@ -16,6 +16,8 @@ import com.digitalasset.canton.ledger.localstore.api.{ PartyRecordUpdate, } import com.digitalasset.canton.logging.LoggingContextWithTrace +import com.digitalasset.daml.lf.data.Ref +import com.digitalasset.daml.lf.data.Ref.{LedgerString, Party} import org.scalatest.freespec.AsyncFreeSpec import scala.language.implicitConversions diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala index 1c2dc28d23..ff6061fca2 100644 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala +++ b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/ledger/localstore/UserStoreTests.scala @@ -165,19 +165,6 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec => } } - "disallow re-creating an existing user concurrently" in { - testIt { tested => - val user = newUser("user1") - for { - res <- Future.sequence( - Seq(tested.createUser(user, Set.empty), tested.createUser(user, Set.empty)) - ) - } yield { - res should contain(Left(UserExists(user.id))) - } - } - } - "deny permission re-creating an existing user within another IDP" in { testIt { tested => val user = newUser("user1") diff --git a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala index 768afdb062..7b77586f64 100644 --- a/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala +++ b/canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/apiserver/services/command/interactive/PreparedTransactionCodecV1Spec.scala @@ -102,40 +102,6 @@ class PreparedTransactionCodecV1Spec } } - "support interfaceId on exercise node" in { - implicit val nodeGen: Arbitrary[Node.Exercise] = Arbitrary( - for { - exerciseNode <- ValueGenerators.danglingRefExerciseNodeGen - normalized = normalizeNodeForV1(exerciseNode).copy( - interfaceId = Some(ValueGenerators.idGen.sample.value) - ) - } yield normalized - ) - - forAll { (node: Node.Exercise) => - val encoded = - encoder.v1.exerciseTransformer(LanguageVersion.v2_1).transform(node).asEither.value - decoder.v1.exerciseTransformer.transform(encoded).asEither.value shouldEqual node - } - } - - "support interfaceId on fetch node" in { - implicit val nodeGen: Arbitrary[Node.Fetch] = Arbitrary( - for { - fetchNode <- ValueGenerators.fetchNodeGen - normalized = normalizeNodeForV1(fetchNode).copy( - interfaceId = Some(ValueGenerators.idGen.sample.value) - ) - } yield normalized - ) - - forAll { (node: Node.Fetch) => - val encoded = - encoder.v1.fetchTransformer(LanguageVersion.v2_1).transform(node).asEither.value - decoder.v1.fetchTransformer.transform(encoded).asEither.value shouldEqual node - } - } - "sort sets of parties" in { forAll { (transaction: VersionedTransaction, nodeSeeds: Option[ImmArray[(NodeId, Hash)]]) => val result = for { diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala deleted file mode 100644 index f746b3c976..0000000000 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiCodecCompressed.scala +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.daml.lf.value.json - -import com.digitalasset.canton.daml.lf.value.json.NavigatorModelAliases as Model -import com.digitalasset.daml.lf.data.ImmArray.ImmArraySeq -import com.digitalasset.daml.lf.data.ScalazEqual.* -import com.digitalasset.daml.lf.data.{ - FrontStack, - ImmArray, - Numeric as LfNumeric, - Ref, - SortedLookupList, - Time, -} -import com.digitalasset.daml.lf.typesig -import com.digitalasset.daml.lf.value.Value as V -import com.digitalasset.daml.lf.value.Value.ContractId -import scalaz.syntax.std.string.* -import scalaz.{@@, Tag} -import spray.json.* - -import java.time.Instant -import scala.annotation.nowarn -import scala.util.Try - -import NavigatorModelAliases.DamlLfIdentifier -import ApiValueImplicits.* - -/** A compressed encoding of API values. - * - * The encoded values do not include type information. For example, it is impossible to distinguish - * party and text values in the encoded format. - * - * Therefore, this JSON format can only decode given a target type. - * - * `apiValueJsonReader` can create a JSON reader with the necessary type information. - * - * @param encodeDecimalAsString - * Not used yet. - * @param encodeInt64AsString - * Not used yet. - */ -class ApiCodecCompressed(val encodeDecimalAsString: Boolean, val encodeInt64AsString: Boolean)( - implicit - readCid: JsonReader[ContractId], - writeCid: JsonWriter[ContractId], -) { self => - - // ------------------------------------------------------------------------------------------------------------------ - // Encoding - // ------------------------------------------------------------------------------------------------------------------ - def apiValueToJsValue(value: V): JsValue = value match { - case v: V.ValueRecord => apiRecordToJsValue(v) - case v: V.ValueVariant => apiVariantToJsValue(v) - case v: V.ValueEnum => apiEnumToJsValue(v) - case v: V.ValueList => apiListToJsValue(v) - case V.ValueText(v) => JsString(v) - case V.ValueInt64(v) => if (encodeInt64AsString) JsString((v: Long).toString) else JsNumber(v) - case V.ValueNumeric(v) => - if (encodeDecimalAsString) JsString(LfNumeric.toUnscaledString(v)) else JsNumber(v) - case V.ValueBool(v) => JsBoolean(v) - case V.ValueContractId(v) => apiContractIdToJsValue(v) - case t: V.ValueTimestamp => JsString(t.toIso8601) - case d: V.ValueDate => JsString(d.toIso8601) - case V.ValueParty(v) => JsString(v) - case V.ValueUnit => JsObject.empty - case V.ValueOptional(None) => JsNull - case V.ValueOptional(Some(v)) => - v match { - case V.ValueOptional(None) => JsArray() - case V.ValueOptional(Some(_)) => JsArray(apiValueToJsValue(v)) - case _ => apiValueToJsValue(v) - } - case textMap: V.ValueTextMap => - apiMapToJsValue(textMap) - case genMap: V.ValueGenMap => - apiGenMapToJsValue(genMap) - } - - @throws[SerializationException] - private[this] final def apiContractIdToJsValue(v: ContractId): JsValue = v.toJson - - private[this] def apiListToJsValue(value: V.ValueList): JsValue = - JsArray(value.values.map(apiValueToJsValue(_)).toImmArray.toSeq*) - - private[this] def apiVariantToJsValue(value: V.ValueVariant): JsValue = - JsonVariant(value.variant, apiValueToJsValue(value.value)) - - private[this] def apiEnumToJsValue(value: V.ValueEnum): JsValue = - JsString(value.value) - - private[ApiCodecCompressed] def apiRecordToJsValue(value: V.ValueRecord): JsValue = { - val namedOrNoneFields = value.fields.toSeq collect { - case (Some(k), v) => Some((k, v)) - case (_, V.ValueOptional(None)) => None - } - if (namedOrNoneFields.lengthIs == value.fields.length) - JsObject(namedOrNoneFields.iterator.collect { case Some((flabel, fvalue)) => - (flabel: String) -> apiValueToJsValue(fvalue) - }.toMap) - else - JsArray(value.fields.toSeq.map { case (_, fvalue) => - apiValueToJsValue(fvalue) - }*) - } - - private[this] def apiMapToJsValue(value: V.ValueTextMap): JsValue = - JsObject( - value.value - .mapValue(apiValueToJsValue) - .toHashMap - ) - - private[this] def apiGenMapToJsValue(value: V.ValueGenMap): JsValue = - JsArray( - value.entries.map { case (key, value) => - JsArray(apiValueToJsValue(key), apiValueToJsValue(value)) - }.toSeq* - ) - - // ------------------------------------------------------------------------------------------------------------------ - // Decoding - this needs access to Daml-LF types - // ------------------------------------------------------------------------------------------------------------------ - - @throws[DeserializationException] - private[this] final def jsValueToApiContractId(value: JsValue): ContractId = - value.convertTo[ContractId] - - @SuppressWarnings(Array("org.wartremover.warts.IterableOps")) - private[this] def jsValueToApiPrimitive( - value: JsValue, - prim: Model.DamlLfTypePrim, - defs: Model.DamlLfTypeLookup, - ): V = - (prim.typ, value).match2 { - case Model.DamlLfPrimType.Int64 => { - case JsString(v) => V.ValueInt64(assertDE(v.parseLong.leftMap(_.getMessage).toEither)) - case JsNumber(v) if v.isValidLong => V.ValueInt64(v.toLongExact) - } - case Model.DamlLfPrimType.Text => { case JsString(v) => V.ValueText(v) } - case Model.DamlLfPrimType.Party => { case JsString(v) => - V.ValueParty(assertDE(Ref.Party fromString v)) - } - case Model.DamlLfPrimType.ContractId => { case v => - V.ValueContractId(jsValueToApiContractId(v)) - } - case Model.DamlLfPrimType.Unit => { case JsObject(_) => V.ValueUnit } - case Model.DamlLfPrimType.Timestamp => { case JsString(v) => - val optTimestamp = for { - instant <- Try(Instant.parse(v)).toEither.left.map(_.getMessage) - timestamp <- Time.Timestamp.fromInstant(instant) - } yield timestamp - V.ValueTimestamp(assertDE(optTimestamp)) - } - case Model.DamlLfPrimType.Date => { case JsString(v) => - try { - V.ValueDate.fromIso8601(v) - } catch { - case _: java.time.format.DateTimeParseException | _: IllegalArgumentException => - throw DeserializationException(s"Invalid date: $v") - } - } - case Model.DamlLfPrimType.Bool => { case JsBoolean(v) => V.ValueBool(v) } - case Model.DamlLfPrimType.List => { case JsArray(v) => - V.ValueList( - v.iterator.map(e => jsValueToApiValue(e, prim.typArgs.head, defs)).to(FrontStack) - ) - } - case Model.DamlLfPrimType.Optional => - val typArg = prim.typArgs.head - val useArray = nestsOptional(prim) - - { - case JsNull => V.ValueNone - case JsArray(ov) if useArray => - ov match { - case Seq() => V.ValueOptional(Some(V.ValueNone)) - case Seq(v) => V.ValueOptional(Some(jsValueToApiValue(v, typArg, defs))) - case _ => - deserializationError(s"Can't read ${value.prettyPrint} as Optional of Optional") - } - case _ if !useArray => V.ValueOptional(Some(jsValueToApiValue(value, typArg, defs))) - } - case Model.DamlLfPrimType.TextMap => { case JsObject(a) => - V.ValueTextMap(SortedLookupList(a.transform { (_, v) => - jsValueToApiValue(v, prim.typArgs.head, defs) - })) - } - case Model.DamlLfPrimType.GenMap => - val Seq(kType, vType) = prim.typArgs: @nowarn("msg=match may not be exhaustive") - - { case JsArray(entries) => - type OK[K] = Vector[(K, V)] - val decEntries: Vector[(V @@ defs.type, V)] = Tag - .subst[V, OK, defs.type](entries.map { - case JsArray(Vector(key, value)) => - jsValueToApiValue(key, kType, defs) -> - jsValueToApiValue(value, vType, defs) - case _ => - deserializationError(s"Can't read ${value.prettyPrint} as key+value of $prim") - }) - V.ValueGenMap(Tag.unsubst[V, OK, defs.type](decEntries).to(ImmArray)) - } - - }(fallback = deserializationError(s"Can't read ${value.prettyPrint} as $prim")) - - private[this] def nestsOptional(prim: typesig.TypePrim): Boolean = - prim match { - case typesig.TypePrim(_, Seq(typesig.TypePrim(typesig.PrimType.Optional, _))) => true - case _ => false - } - - private[this] def jsValueToApiDataType( - value: JsValue, - id: DamlLfIdentifier, - dt: Model.DamlLfDataType, - defs: Model.DamlLfTypeLookup, - ): V = - (dt, value).match2 { - case Model.DamlLfRecord(fields) => { - case JsObject(v) => - V.ValueRecord( - Some(id), - fields.map { case (fName, fTy) => - val fValue = v - .get(fName) - .map(jsValueToApiValue(_, fTy, defs)) - .getOrElse(fTy match { - case typesig.TypePrim(typesig.PrimType.Optional, _) => V.ValueNone - case _ => - deserializationError( - s"Can't read ${value.prettyPrint} as DamlLfRecord $id, missing field '$fName'" - ) - }) - (Some(fName), fValue) - }.toImmArray, - ) - case JsArray(fValues) => - if (fValues.sizeCompare(fields) != 0) - deserializationError( - s"Can't read ${value.prettyPrint} as DamlLfRecord $id, wrong number of record fields (expected ${fields.length}, found ${fValues.length})." - ) - else - V.ValueRecord( - Some(id), - (fields zip fValues).map { case ((fName, fTy), fValue) => - (Some(fName), jsValueToApiValue(fValue, fTy, defs)) - }.toImmArray, - ) - } - case Model.DamlLfVariant(cons) => { - case JsonVariant(tag, nestedValue) => - val (constructorName, constructorType) = cons.toList - .find(_._1 == tag) - .getOrElse( - deserializationError( - s"Can't read ${value.compactPrint} as DamlLfVariant $id, unknown constructor $tag" - ) - ) - - V.ValueVariant( - Some(id), - constructorName, - jsValueToApiValue(nestedValue, constructorType, defs), - ) - case _ => - deserializationError( - s"Can't read ${value.prettyPrint} as DamlLfVariant $id, expected JsObject with 'tag' and 'value' fields" - ) - } - case Model.DamlLfEnum(cons) => { case JsString(c) => - cons - .collectFirst { case kc if kc == c => kc } - .map( - V.ValueEnum( - Some(id), - _, - ) - ) - .getOrElse( - deserializationError( - s"Can't read ${value.prettyPrint} as DamlLfEnum $id, unknown constructor $c" - ) - ) - } - }(fallback = deserializationError(s"Can't read ${value.prettyPrint} as $dt")) - - /** Deserialize a value, given the type */ - def jsValueToApiValue( - value: JsValue, - typ: Model.DamlLfType, - defs: Model.DamlLfTypeLookup, - ): V = - typ match { - case prim: Model.DamlLfTypePrim => - jsValueToApiPrimitive(value, prim, defs) - case typeCon: Model.DamlLfTypeCon => - val id = typeCon.tycon.identifier - val dt = - typeCon.instantiate(defs(id).getOrElse(deserializationError(s"Type $id not found"))) - jsValueToApiDataType(value, id, dt, defs) - case Model.DamlLfTypeNumeric(scale) => - val numericOrError = value match { - case JsString(v) => - LfNumeric.checkWithinBoundsAndRound(scale, BigDecimal(v)) - case JsNumber(v) => - LfNumeric.checkWithinBoundsAndRound(scale, v) - case _ => - deserializationError(s"Can't read ${value.prettyPrint} as (Numeric $scale)") - } - V.ValueNumeric(assertDE(numericOrError)) - case Model.DamlLfTypeVar(_) => - deserializationError(s"Can't read ${value.prettyPrint} as DamlLfTypeVar") - } - - /** Deserialize a value, given the ID of the corresponding closed type */ - def jsValueToApiValue( - value: JsValue, - id: Model.DamlLfIdentifier, - defs: Model.DamlLfTypeLookup, - ): V = { - val typeCon = Model.DamlLfTypeCon(Model.DamlLfTypeConId(id), ImmArraySeq()) - val dt = typeCon.instantiate(defs(id).getOrElse(deserializationError(s"Type $id not found"))) - jsValueToApiDataType(value, id, dt, defs) - } - - private[this] def assertDE[A](ea: Either[String, A]): A = - ea.fold(deserializationError(_), identity) - - private[json] def copy( - encodeDecimalAsString: Boolean = this.encodeDecimalAsString, - encodeInt64AsString: Boolean = this.encodeInt64AsString, - ): ApiCodecCompressed = - new ApiCodecCompressed( - encodeDecimalAsString = encodeDecimalAsString, - encodeInt64AsString = encodeInt64AsString, - ) -} - -private[json] object JsonContractIdFormat { - implicit val ContractIdFormat: JsonFormat[ContractId] = - new JsonFormat[ContractId] { - override def write(obj: ContractId) = - JsString(obj.coid) - override def read(json: JsValue) = json match { - case JsString(s) => - ContractId.fromString(s).fold(deserializationError(_), identity) - case _ => deserializationError("ContractId must be a string") - } - } -} -import JsonContractIdFormat.* - -object ApiCodecCompressed - extends ApiCodecCompressed(encodeDecimalAsString = true, encodeInt64AsString = true) { - // ------------------------------------------------------------------------------------------------------------------ - // Implicits that can be imported to write JSON - // ------------------------------------------------------------------------------------------------------------------ - object JsonImplicits extends DefaultJsonProtocol { - implicit object ApiValueJsonFormat extends RootJsonWriter[Model.ApiValue] { - def write(v: Model.ApiValue): JsValue = apiValueToJsValue(v) - } - implicit object ApiRecordJsonFormat extends RootJsonWriter[Model.ApiRecord] { - def write(v: Model.ApiRecord): JsValue = apiRecordToJsValue(v) - } - - implicit val ContractIdFormat: JsonFormat[ContractId] = JsonContractIdFormat.ContractIdFormat - } -} diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala deleted file mode 100644 index a581056cb6..0000000000 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/ApiValueImplicits.scala +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.daml.lf.value.json - -import com.digitalasset.daml.lf.data.Time -import com.digitalasset.daml.lf.value.Value as V - -import java.time.format.DateTimeFormatter -import java.time.{Instant, LocalDate} - -object ApiValueImplicits { - - implicit final class `ApiTimestamp additions`(private val it: V.ValueTimestamp) extends AnyVal { - import it.* - def toInstant: Instant = value.toInstant - def toIso8601: String = DateTimeFormatter.ISO_INSTANT.format(toInstant) - } - - implicit final class `ApiDate additions`(private val it: V.ValueDate) extends AnyVal { - import it.* - def toLocalDate: LocalDate = LocalDate.ofEpochDay((value.days: Int).toLong) - def toInstant: Instant = Instant.from(toLocalDate) - def toIso8601: String = DateTimeFormatter.ISO_LOCAL_DATE.format(toLocalDate) - } - - // Timestamp has microsecond resolution - implicit final class `ApiTimestamp.type additions`(private val it: V.ValueTimestamp.type) - extends AnyVal { - def fromIso8601(t: String): V.ValueTimestamp = fromInstant(Instant.parse(t)) - def fromInstant(t: Instant): V.ValueTimestamp = - V.ValueTimestamp(Time.Timestamp.assertFromInstant(t)) - } - - implicit final class `ApiDate.type additions`(private val it: V.ValueDate.type) extends AnyVal { - def fromIso8601(t: String): V.ValueDate = - fromLocalDate(LocalDate.parse(t, DateTimeFormatter.ISO_LOCAL_DATE)) - def fromLocalDate(t: LocalDate): V.ValueDate = - V.ValueDate(Time.Date.assertFromDaysSinceEpoch(t.toEpochDay.toInt)) - } -} diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala deleted file mode 100644 index 87a33ea5b0..0000000000 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/JsonVariant.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.daml.lf.value.json - -import com.digitalasset.daml.lf.data.Ref -import spray.json.{JsObject, JsString, JsValue} - -object JsonVariant { - def apply(tag: String, body: JsValue): JsObject = - JsObject(tagKey -> JsString(tag), valueKey -> body) - - val tagKey = "tag" - val valueKey: Ref.Name = Ref.Name assertFromString "value" - - def unapply(o: JsObject): Option[(String, JsValue)] = - (o.fields.size, o.fields.get(tagKey), o.fields.get(valueKey)) match { - case (2, Some(JsString(tag)), Some(nv)) => Some((tag, nv)) - case _ => None - } -} diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala deleted file mode 100644 index 358acf922f..0000000000 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/daml/lf/value/json/NavigatorModelAliases.scala +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.daml.lf.value.json - -import com.digitalasset.daml.lf.data.Ref as DamlLfRef -import com.digitalasset.daml.lf.typesig -import com.digitalasset.daml.lf.value.Value as V - -/** Aliases used by navigator's Scala backend, from which this package was derived. - */ -trait NavigatorModelAliases[Cid] { - - /** An absolute reference of a Daml-LF entity. Contains a Daml-LF package ID and a qualified name. - * Currently, such identifiers can point to: - * - Templates - * - User-defined records - * - User-defined variants - */ - type DamlLfIdentifier = DamlLfRef.Identifier - val DamlLfIdentifier = DamlLfRef.Identifier - - /** A simple Daml-LF type Currently, these can be: - * - Primitive types - * - Type constructor applications (i.e., dereferencing a DamlLfIdentifier) - * - Type variables - */ - type DamlLfType = typesig.Type - type DamlLfTypeCon = typesig.TypeCon - val DamlLfTypeCon = typesig.TypeCon - type DamlLfTypePrim = typesig.TypePrim - val DamlLfTypePrim = typesig.TypePrim - type DamlLfTypeVar = typesig.TypeVar - val DamlLfTypeVar = typesig.TypeVar - type DamlLfTypeConId = typesig.TypeConId - val DamlLfTypeConId = typesig.TypeConId - type DamlLfTypeNumeric = typesig.TypeNumeric - val DamlLfTypeNumeric = typesig.TypeNumeric - - type DamlLfPrimType = typesig.PrimType - val DamlLfPrimType = typesig.PrimType - - /** A user-defined Daml-LF type (closed form). Can be a record or variant. */ - type DamlLfDataType = typesig.DataType.FWT - val DamlLfDataType = typesig.DataType - - /** A user-defined Daml-LF type (generic form). Can be a record or variant. */ - type DamlLfDefDataType = typesig.DefDataType.FWT - - type DamlLfTypeLookup = DamlLfIdentifier => Option[DamlLfDefDataType] - - /** A user-defined Daml-LF record */ - type DamlLfRecord = typesig.Record.FWT - val DamlLfRecord = typesig.Record - - /** A user-defined Daml-LF variant */ - type DamlLfVariant = typesig.Variant.FWT - val DamlLfVariant = typesig.Variant - - /** A user-defined Daml-LF enum */ - type DamlLfEnum = typesig.Enum - val DamlLfEnum = typesig.Enum - - type ApiValue = V - type ApiRecord = V.ValueRecord -} - -object NavigatorModelAliases extends NavigatorModelAliases[String] diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala index 2ee846574e..757fe2b274 100644 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala +++ b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/Endpoints.scala @@ -15,7 +15,7 @@ import com.digitalasset.canton.ledger.error.{JsonApiErrors, LedgerApiErrors} import com.digitalasset.canton.logging.audit.ApiRequestLogger import com.digitalasset.canton.logging.{LoggingContextWithTrace, NamedLogging} import com.digitalasset.canton.networking.grpc.CallMetadata -import com.digitalasset.canton.tracing.{HeaderName, TraceContext, W3CTraceContext} +import com.digitalasset.canton.tracing.{TraceContext, W3CTraceContext} import com.digitalasset.daml.lf.data.Ref import com.digitalasset.daml.lf.engine.Error.Preprocessing import com.digitalasset.daml.lf.language.Ast.TVar @@ -465,9 +465,7 @@ object Endpoints { ) ) .map { case (headersList: Seq[Header], addr) => - val z = W3CTraceContext.fromHeaders( - headersList.map(h => (HeaderName(h.name), h.value)).toMap - ) + val z = W3CTraceContext.fromHeaders(headersList.map(h => (h.name, h.value)).toMap) (z.map(_.toTraceContext), addr) } { case (tc1, addr) => ( diff --git a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala index f68997df4c..463bf91813 100644 --- a/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala +++ b/canton/community/ledger/ledger-json-api/src/main/scala/com/digitalasset/canton/http/json/v2/V2Routes.scala @@ -16,7 +16,7 @@ import com.digitalasset.canton.logging.audit.{ApiRequestLogger, ResponseKind, Tr import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.CallMetadata import com.digitalasset.canton.platform.PackagePreferenceBackend -import com.digitalasset.canton.tracing.{HeaderName, TraceContext, W3CTraceContext} +import com.digitalasset.canton.tracing.{TraceContext, W3CTraceContext} import org.apache.pekko.http.scaladsl.model.{AttributeKeys, HttpRequest, MediaType, MediaTypes} import org.apache.pekko.http.scaladsl.server.RequestContext import org.apache.pekko.stream.Materializer @@ -193,7 +193,7 @@ class RequestInterceptors( def loggingInterceptor() = RequestInterceptor.transformServerRequest { request => - val incomingHeaders = request.headers.map(h => (HeaderName(h.name), h.value)).toMap + val incomingHeaders = request.headers.map(h => (h.name, h.value)).toMap val extractedW3cTrace = W3CTraceContext.fromHeaders(incomingHeaders) val requestParameters = s"[${request.queryParameters.toSeq.map { case (k, v) => s"$k=$v" }.mkString(", ")}]" @@ -289,7 +289,7 @@ class RequestInterceptors( def apply[B](request: ServerRequest, result: RequestResult[B]): Future[RequestResult[B]] = { val addr = RequestInterceptorsUtil.extractAddress(request) - val incomingHeaders = request.headers.map(h => (HeaderName(h.name), h.value)).toMap + val incomingHeaders = request.headers.map(h => (h.name, h.value)).toMap val extractedW3cTrace = W3CTraceContext.fromHeaders(incomingHeaders) val callMetadata = CallMetadata( apiEndpoint = request.showShort, diff --git a/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java b/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java index 152855d19f..4ac3d6c629 100644 --- a/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java +++ b/canton/community/lib/Blake2b/src/main/java/org/bouncycastle/crypto/digests/canton/Blake2bDigest.java @@ -3,12 +3,12 @@ /* The BLAKE2 cryptographic hash function was designed by Jean- Philippe Aumasson, Samuel Neves, Zooko Wilcox-O'Hearn, and Christian Winnerlein. - - Reference Implementation and Description can be found at: https://blake2.net/ + + Reference Implementation and Description can be found at: https://blake2.net/ Internet Draft: https://tools.ietf.org/html/draft-saarinen-blake2-02 - This implementation does not support the Tree Hashing Mode. - + This implementation does not support the Tree Hashing Mode. + For unkeyed hashing, developers adapting BLAKE2 to ASN.1 - based message formats SHOULD use the OID tree at x = 1.3.6.1.4.1.1722.12.2. diff --git a/canton/community/lib/README.md b/canton/community/lib/README.md index b0039fb67c..0137b12fd5 100644 --- a/canton/community/lib/README.md +++ b/canton/community/lib/README.md @@ -1,12 +1,11 @@ Forked or extended Daml and third-party libraries: -- [daml](https://github.com/digital-asset/daml) - - Explanation and details described in [CONTRIBUTING.md](../CONTRIBUTING.md) under "Managing Daml repo upstream "mismatches"" - - [BouncyCastle Blake2b](http://git.bouncycastle.org/repositories/bc-java) - Blake2b implementation in Java. Forked as the original doesn't support the parameters required to build Blake2xb on top of Blake2b. -- [functionmeta](https://github.com/katlasik/functionmeta) - Macros to retrieve function metadata. Forked to also get a function's full name. - - [slick](https://github.com/slick/slick) - Modified sql interpolations to set read and write effects -- [wartremover](http://www.wartremover.org/) - Implementations of custom warts \ No newline at end of file +- [wartremover](http://www.wartremover.org/) - Implementations of custom warts + +- [magnolify](https://github.com/spotify/magnolify) - Implementations of our own type class derivations + +- [scalatest](https://www.scalatest.org/) - Modified assertions to test for compilation errors diff --git a/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala b/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala index e78c8b850b..4ef1d383da 100644 --- a/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala +++ b/canton/community/lib/slick/src/main/scala/slick/jdbc/canton/StaticQuery.scala @@ -117,11 +117,6 @@ case class SQLActionBuilder(sql: String, setParameter: SetParameter[Unit]) { ] = asInternal[Int, Effect.Write](GetResult.GetUpdateValue).head - def asUpdateReturning[R](implicit - rconv: GetResult[R] - ): SqlStreamingAction[Vector[R], R, Effect.Read & Effect.Write] = - asInternal[R, Effect.Read & Effect.Write] - def concat(b: SQLActionBuilder): SQLActionBuilder = SQLActionBuilder( sql + b.sql, diff --git a/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/DirectGrpcServiceInvocation.scala b/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/DirectGrpcServiceInvocation.scala index 101741d6e8..74a0949e98 100644 --- a/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/DirectGrpcServiceInvocation.scala +++ b/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/DirectGrpcServiceInvocation.scala @@ -66,8 +66,8 @@ object DirectGrpcServiceInvocation extends WartTraverser { tree.symbol.annotations.exists(isGrpcServiceInvocationMethodAnnotation) def isMethodDefWithGrpcServiceInvocationMethodAnnotation(symbol: Symbol): Boolean = - symbol.annotations.exists(isGrpcServiceInvocationMethodAnnotation) || symbol.overrides - .exists(isMethodDefWithGrpcServiceInvocationMethodAnnotation) + symbol.annotations.exists(isGrpcServiceInvocationMethodAnnotation) + || symbol.overrides.exists(isMethodDefWithGrpcServiceInvocationMethodAnnotation) def isArtifactValDef(x: Tree): Boolean = x match { diff --git a/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/EnforceVisibleForTesting.scala b/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/EnforceVisibleForTesting.scala index 3b776b1e59..500cd6ba8b 100644 --- a/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/EnforceVisibleForTesting.scala +++ b/canton/community/lib/wartremover/src/main/scala-2/com/digitalasset/canton/EnforceVisibleForTesting.scala @@ -39,9 +39,9 @@ object EnforceVisibleForTesting extends WartTraverser { def currentlyVisitingParents(symbol: Symbol): Boolean = if (symbol == null || symbol == u.universe.NoSymbol || symbol.isPackage) false else - (symbol.owner != null && currentlyVisitingDefitions.contains( - symbol.owner.fullName - )) || currentlyVisitingParents(symbol.owner) + (symbol.owner != null + && currentlyVisitingDefitions.contains(symbol.owner.fullName)) + || currentlyVisitingParents(symbol.owner) @SuppressWarnings(Array("org.wartremover.warts.Var", "org.wartremover.warts.While")) object AccessToMemberWithVisibleforTestingAnnotation { diff --git a/canton/community/lib/wartremover/src/test/scala/com/digitalasset/canton/RequireBlockingTest.scala b/canton/community/lib/wartremover/src/test/scala/com/digitalasset/canton/RequireBlockingTest.scala index a5c45e48e6..6b97595fe7 100644 --- a/canton/community/lib/wartremover/src/test/scala/com/digitalasset/canton/RequireBlockingTest.scala +++ b/canton/community/lib/wartremover/src/test/scala/com/digitalasset/canton/RequireBlockingTest.scala @@ -58,6 +58,7 @@ class RequireBlockingTest extends AnyWordSpec with Matchers { import x.synchronized as foo foo(19) } + if (ScalaVersion.isScala3) assertIsErrorSynchronized(result) else result.errors shouldBe Seq.empty } diff --git a/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-party-replication-alpha/daml.yaml b/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-party-replication-alpha/daml.yaml index 1c192cac9f..df859525c8 100644 --- a/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-party-replication-alpha/daml.yaml +++ b/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-party-replication-alpha/daml.yaml @@ -1,9 +1,15 @@ # Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -sdk-version: 3.5.2 +override-components: + damlc: + version: $DAML_VERSION + daml-script: + version: $DAML_VERSION + codegen: + version: $DAML_VERSION build-options: -- --target=2.1 +- --target=2.2 name: canton-builtin-admin-workflow-party-replication-alpha source: Canton/Internal/PartyReplication.daml version: 1.0.0 diff --git a/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-ping/daml.yaml b/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-ping/daml.yaml index 3a110cc029..d429002328 100644 --- a/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-ping/daml.yaml +++ b/canton/community/participant/src/main/daml/canton-builtin-admin-workflow-ping/daml.yaml @@ -1,9 +1,15 @@ # Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -sdk-version: 3.5.2 +override-components: + damlc: + version: $DAML_VERSION + daml-script: + version: $DAML_VERSION + codegen: + version: $DAML_VERSION build-options: -- --target=2.1 +- --target=2.2 name: canton-builtin-admin-workflow-ping source: CantonBuiltinAdminWorkflowPing.daml version: 1.0.0 diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcPartyManagementService.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcPartyManagementService.scala index 2e933691bf..038f5ac53f 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcPartyManagementService.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcPartyManagementService.scala @@ -467,7 +467,8 @@ class GrpcPartyManagementService( topologyTransaction.events.exists { event => // Search for onboarding or added event and let caller decide whether we found the right event (event.event.isParticipantAuthorizationOnboarding && - event.getParticipantAuthorizationOnboarding.participantId == targetParticipant.uid.toProtoPrimitive) || (event.event.isParticipantAuthorizationAdded && + event.getParticipantAuthorizationOnboarding.participantId == targetParticipant.uid.toProtoPrimitive) + || (event.event.isParticipantAuthorizationAdded && event.getParticipantAuthorizationAdded.participantId == targetParticipant.uid.toProtoPrimitive) } } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala index 8ade10072b..ff89748066 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/grpc/GrpcSynchronizerConnectivityService.scala @@ -6,7 +6,6 @@ package com.digitalasset.canton.participant.admin.grpc import cats.data.EitherT import cats.syntax.bifunctor.* import cats.syntax.either.* -import cats.syntax.option.* import cats.syntax.traverse.* import com.digitalasset.canton.ProtoDeserializationError.ProtoDeserializationFailure import com.digitalasset.canton.admin.participant.v30 @@ -27,7 +26,6 @@ import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, import com.digitalasset.canton.networking.grpc.CantonGrpcUtil import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors.AbortedDueToShutdown import com.digitalasset.canton.participant.admin.data.ManualLsuRequest -import com.digitalasset.canton.participant.store.SynchronizerConnectionConfigStore import com.digitalasset.canton.participant.sync.SyncServiceError.SyncServiceInternalError.{ PhysicalSynchronizerIdNotConfigured, SynchronizerIsMissingInternally, @@ -242,45 +240,18 @@ class GrpcSynchronizerConnectivityService( override def listRegisteredSynchronizers( request: v30.ListRegisteredSynchronizersRequest ): Future[v30.ListRegisteredSynchronizersResponse] = { - val connected = sync.readySynchronizers.values.map { case (psid, _) => psid }.toSet + val connected = sync.readySynchronizers val registeredSynchronizers = sync.registeredSynchronizers - def toProtoStatus(status: SynchronizerConnectionConfigStore.Status) = { - import v30.ListRegisteredSynchronizersResponse.Status - - status match { - case SynchronizerConnectionConfigStore.Active => Status.STATUS_ACTIVE - case SynchronizerConnectionConfigStore.HardMigratingSource => - Status.STATUS_HARD_MIGRATING_SOURCE - case SynchronizerConnectionConfigStore.HardMigratingTarget => - Status.STATUS_HARD_MIGRATING_TARGET - case SynchronizerConnectionConfigStore.LsuSource => Status.STATUS_LSU_SOURCE - case SynchronizerConnectionConfigStore.LsuTarget => Status.STATUS_LSU_TARGET - case SynchronizerConnectionConfigStore.Inactive => Status.STATUS_INACTIVE - } - } - Future.successful( v30.ListRegisteredSynchronizersResponse( results = registeredSynchronizers - .filter(_.status.isActive || request.allStatuses) + .filter(_.status.isActive) .map(cnf => new v30.ListRegisteredSynchronizersResponse.Result( config = Some(cnf.config.toProtoV30), - /* - The configured psid is set after the first connect. Hence, if empty, - it is safe to assume that we are not connected to the node. - */ - connected = cnf.configuredPsid.toOption.exists(connected.contains), + connected = connected.contains(cnf.config.synchronizerAlias), physicalSynchronizerId = cnf.configuredPsid.toOption.map(_.toProtoPrimitive), - status = toProtoStatus(cnf.status), - synchronizerPredecessor = cnf.predecessor.map { predecessor => - com.digitalasset.canton.admin.topology.v30.SynchronizerPredecessor( - predecessorPhysicalId = predecessor.psid.toProtoPrimitive, - upgradeTime = predecessor.upgradeTime.toProtoTimestamp.some, - isLateUpgrade = predecessor.isLateUpgrade, - ) - }, ) ) ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicator.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicator.scala index ed805ed4ed..baf10be71c 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicator.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/admin/party/PartyReplicator.scala @@ -1093,15 +1093,17 @@ final class PartyReplicator( ): Seq[PartyReplicationStateManager.Modification] = if (condition) Seq(update) else Seq.empty - statusUpdate(isAgreementArchived, _.setAgreementO(None)) ++ statusUpdate( - isOnboardingFlagVerifiedCleared, - _.setAuthorization( - PartyReplicationAuthorization(onboardingAt, isOnboardingFlagCleared = true) - ), - ) ++ statusUpdate( - (isAgreementArchived || agreementO.isEmpty) && (isOnboardingFlagVerifiedCleared || isOnboardingFlagCleared), - _.setCompleted(), - ) + statusUpdate(isAgreementArchived, _.setAgreementO(None)) + ++ statusUpdate( + isOnboardingFlagVerifiedCleared, + _.setAuthorization( + PartyReplicationAuthorization(onboardingAt, isOnboardingFlagCleared = true) + ), + ) + ++ statusUpdate( + (isAgreementArchived || agreementO.isEmpty) && (isOnboardingFlagVerifiedCleared || isOnboardingFlagCleared), + _.setCompleted(), + ) } // Delete the items from the party replication indexing store since all contract diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/AssignmentValidationResult.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/AssignmentValidationResult.scala index 9d751282ef..b611d0333d 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/AssignmentValidationResult.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/AssignmentValidationResult.scala @@ -58,8 +58,8 @@ final case class AssignmentValidationResult private[reassignment] ( val isReassignmentActive: Boolean = !reassigningParticipantValidationResult.isUnassignmentDataNotFound && !reassigningParticipantValidationResult.isAssignmentCompleted && - commonValidationResult.activenessResult.inactiveReassignments.contains(reassignmentId) && - commonValidationResult.activenessResult.contracts.isSuccessful + commonValidationResult.activenessResult.inactiveReassignments.contains(reassignmentId) + && commonValidationResult.activenessResult.contracts.isSuccessful commonValidationResult.activenessResult.isSuccessful || isReassignmentActive } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentCoordination.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentCoordination.scala index 4e6d6cba82..72e63f459d 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentCoordination.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/reassignment/ReassignmentCoordination.scala @@ -254,9 +254,8 @@ class ReassignmentCoordination( _.traverseSingleton((_, syncCrypto) => syncCrypto.snapshot(timestamp.unwrap)) ) - private def awaitTimestampAndGetTaggedCryptoSnapshot[T[X] <: ReassignmentTag[ - X - ]: SameReassignmentType: SingletonTraverse]( + private def awaitTimestampAndGetTaggedCryptoSnapshot[T[X] <: ReassignmentTag[X] + : SameReassignmentType: SingletonTraverse]( targetSynchronizerId: T[PhysicalSynchronizerId], staticSynchronizerParameters: T[StaticSynchronizerParameters], timestamp: T[CantonTimestamp], @@ -281,9 +280,8 @@ class ReassignmentCoordination( import cats.implicits.* - private def getRecentTopologyTimestamp[T[X] <: ReassignmentTag[ - X - ]: SameReassignmentType: SingletonTraverse]( + private def getRecentTopologyTimestamp[T[X] <: ReassignmentTag[X] + : SameReassignmentType: SingletonTraverse]( psid: T[PhysicalSynchronizerId] )(implicit traceContext: TraceContext @@ -333,9 +331,8 @@ class ReassignmentCoordination( } } yield topology - private def getTopologyClient[ - T[X] <: ReassignmentTag[X]: SameReassignmentType: SingletonTraverse - ]( + private def getTopologyClient[T[X] <: ReassignmentTag[X] + : SameReassignmentType: SingletonTraverse]( psid: T[PhysicalSynchronizerId], staticSynchronizerParameters: T[StaticSynchronizerParameters], ): Either[UnknownPhysicalSynchronizer, T[SynchronizerCryptoClient]] = diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/EncryptedViewMessageFactory.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/EncryptedViewMessageFactory.scala index c43a41894b..db73931b8f 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/EncryptedViewMessageFactory.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/EncryptedViewMessageFactory.scala @@ -206,7 +206,7 @@ object EncryptedViewMessageFactory { ): EitherT[FutureUnlessShutdown, EncryptedViewMessageCreationError, NonEmpty[Seq[ EncryptedSingleViewMessage[ VT - ] + ], ]]] = { val viewEncryptionScheme = cryptoSnapshot.pureCrypto.defaultSymmetricKeyScheme diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/LegacyTransactionTreeFactory.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/LegacyTransactionTreeFactory.scala index 5c848319a2..c388fb4453 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/LegacyTransactionTreeFactory.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/submission/LegacyTransactionTreeFactory.scala @@ -15,7 +15,7 @@ import com.digitalasset.canton.crypto.{HashOps, HmacOps, Salt, SaltSeed} import com.digitalasset.canton.data.* import com.digitalasset.canton.data.TransactionViewDecomposition.{NewView, SameView} import com.digitalasset.canton.data.ViewConfirmationParameters.InvalidViewConfirmationParameters -import com.digitalasset.canton.data.{ +import com.digitalasset.canton.data.{ // scalafix:ok OrganizeImports LegacyAssignedKey as AssignedKey, LegacyFreeKey as FreeKey, LegacySerializableKeyResolution as SerializableKeyResolution, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/CommitmentCommandsHelper.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/CommitmentCommandsHelper.scala index 9df45088a0..2a3beb35cb 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/CommitmentCommandsHelper.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/CommitmentCommandsHelper.scala @@ -103,7 +103,7 @@ final case class CommitmentContractMetadata( object CommitmentContractMetadata extends HasVersionedMessageCompanion[ - CommitmentContractMetadata + CommitmentContractMetadata, ] { import JsonCodecs.* @SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.Var")) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/FirstUnsafeOffsetComputation.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/FirstUnsafeOffsetComputation.scala index d2ff4aa49f..db9e4d653a 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/FirstUnsafeOffsetComputation.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/pruning/FirstUnsafeOffsetComputation.scala @@ -409,9 +409,8 @@ object FirstUnsafeOffsetComputation { safeCommitmentTick -> "ACS background reconciliation", ) ++ synchronizerIndex .flatMap(_.sequencerIndex) - .map(_ -> "Synchronizer index crash recovery") ++ earliestInFlight.map( - _ -> "inFlightSubmissionTs" - ) + .map(_ -> "Synchronizer index crash recovery") + ++ earliestInFlight.map(_ -> "inFlightSubmissionTs") _ = errorLoggingContext .debug( diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbActiveContractStore.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbActiveContractStore.scala index 7489fb756e..47a2f752da 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbActiveContractStore.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/store/db/DbActiveContractStore.scala @@ -728,7 +728,8 @@ class DbActiveContractStore( val query = { sql"""select ts, repair_counter, contract_id, operation, reassignment_counter, remote_synchronizer_idx from par_active_contracts where synchronizer_idx = $indexedSynchronizer - and (ts, repair_counter) = ($sequenceTs, $sequenceRc)""" ++ withCidLowerBound ++ sql""" + and (ts, repair_counter) = ($sequenceTs, $sequenceRc)""" + ++ withCidLowerBound ++ sql""" -- grouped by contract_id, so that activations and deactivations are as close as they can be and it's more likely to find the reassignment counter for deactivations in the same batch order by contract_id asc, -- in the order first activation, then deactivation diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala index 5864190753..5c3636fdb2 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/topology/TopologyLookup.scala @@ -96,7 +96,7 @@ final class TopologyLookup( } snapshot <- topologyClientFor(psid).biflatMap( - _ => offlineTopologyClient(psid).map(_.approximateTimestamp), + _ => offlineTopologyClient(psid).map(_.approximateTimestamp).toEitherT[FutureUnlessShutdown], topologyClient => EitherT.rightT[FutureUnlessShutdown, ParticipantTopologyManagerError]( topologyClient.approximateTimestamp @@ -121,10 +121,9 @@ final class TopologyLookup( psid: PhysicalSynchronizerId )(implicit traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, ParticipantTopologyManagerError, SynchronizerTopologyClient] = - for { - syncPersistentState <- EitherT.fromEither[FutureUnlessShutdown](syncPersistentStateFor(psid)) - client = new StoreBasedSynchronizerTopologyClient( + ): Either[ParticipantTopologyManagerError, SynchronizerTopologyClient] = + syncPersistentStateFor(psid).map { syncPersistentState => + new StoreBasedSynchronizerTopologyClient( clock = clock, staticSynchronizerParameters = syncPersistentState.staticSynchronizerParameters, store = syncPersistentState.topologyStore, @@ -134,10 +133,7 @@ final class TopologyLookup( futureSupervisor = futureSupervisor, loggerFactory = loggerFactory, ) - - _ <- EitherT.liftF(client.updateKnownTimestampsDuringStartup()) - - } yield client + } private def maybeOfflineTopologyClient(synchronizer: Synchronizer)(implicit traceContext: TraceContext @@ -149,7 +145,7 @@ final class TopologyLookup( EitherT.pure[FutureUnlessShutdown, ParticipantTopologyManagerError](psid) } client <- topologyClientFor(psid).biflatMap( - _ => offlineTopologyClient(psid), + _ => offlineTopologyClient(psid).toEitherT[FutureUnlessShutdown], topologyClient => EitherT.pure[FutureUnlessShutdown, ParticipantTopologyManagerError](topologyClient), ) diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala index 860192280c..1ab25df8b0 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/util/DAMLe.scala @@ -379,21 +379,7 @@ class DAMLe( else NeedKeyProgression.Finished FutureUnlessShutdown .pure(result) - .flatMap { r => - val entries = r.map { contract => - CantonContractIdVersion.extractCantonContractIdVersion(contract.contractId) match { - case Right(version) => - ResultNeedKey.Response.AuthenticableFatContractInstance( - contract, - version.contractHashingMethod, - hash => contractAuthenticator(contract, hash).isRight, - ) - case Left(_) => - ResultNeedKey.Response.UnsupportedContractIdVersion(contract.contractId) - } - } - handleResultInternal(resume(ResultNeedKey.Response(entries, hasStarted))) - } + .flatMap(r => handleResultInternal(resume(r, hasStarted))) case ResultNeedContract(acoid, resume) => val response: Response = diff --git a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala b/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala deleted file mode 100644 index 65a9906158..0000000000 --- a/canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.participant.topology - -import com.digitalasset.canton.concurrent.FutureSupervisor -import com.digitalasset.canton.config.TopologyConfig -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.participant.store.SyncPersistentState -import com.digitalasset.canton.topology.DefaultTestIdentities.defaultDynamicSynchronizerParameters -import com.digitalasset.canton.topology.admin.grpc.PsidLookup -import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime} -import com.digitalasset.canton.topology.store.TopologyStoreId.SynchronizerStore -import com.digitalasset.canton.topology.store.memory.InMemoryTopologyStore -import com.digitalasset.canton.topology.store.{TopologyStoreTestData, ValidatedTopologyTransaction} -import com.digitalasset.canton.topology.transaction.SynchronizerParametersState -import com.digitalasset.canton.topology.{ - DefaultTestIdentities, - PhysicalSynchronizerId, - SynchronizerId, - TopologyManagerError, -} -import com.digitalasset.canton.{BaseTest, HasExecutionContext} -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike - -final class TopologyLookupTest - extends BaseTest - with AnyWordSpecLike - with Matchers - with HasExecutionContext { - "TopologyLookup" should { - "allow to create offline topology client from topology store" in { - val psid = DefaultTestIdentities.physicalSynchronizerId - val otherPsid = psid.incrementSerial - - val persistentState = mock[SyncPersistentState] - - val topologyStoreTestData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, parallelExecutionContext) - - val topologyStore = new InMemoryTopologyStore[SynchronizerStore]( - SynchronizerStore(psid), - predecessor = None, - testedProtocolVersion, - loggerFactory, - timeouts, - ) - - val ts = CantonTimestamp.now() - val mapping = SynchronizerParametersState(psid.logical, defaultDynamicSynchronizerParameters) - - val signedTx = topologyStoreTestData.makeSignedTx(mapping)(topologyStoreTestData.p1Key) - - topologyStore - .update( - SequencedTime(ts), - EffectiveTime(ts), - removals = Map(), - Seq(ValidatedTopologyTransaction(signedTx)), - ) - .futureValueUS - - when(persistentState.staticSynchronizerParameters).thenAnswer( - defaultStaticSynchronizerParameters - ) - when(persistentState.topologyStore).thenAnswer(topologyStore) - - val topologyLookup = new TopologyLookup( - clock = wallClock, - topologyConfig = TopologyConfig(), - timeouts = timeouts, - futureSupervisor = FutureSupervisor.Noop, - topologyManagerO = _ => None, // Simulate offline - topologyClientO = _ => None, // Simulate offline - psidLookup = new PsidLookup { - override def activePsidFor( - synchronizerId: SynchronizerId - ): Option[PhysicalSynchronizerId] = None - }, - syncPersistentStateO = - (id: PhysicalSynchronizerId) => if (id == psid) Some(persistentState) else None, - loggerFactory = loggerFactory, - ) - - val approximateSnapshot = - topologyLookup.maybeOfflineApproximateSnapshot(psid).futureValueUS.value - approximateSnapshot.timestamp shouldBe ts.immediateSuccessor - approximateSnapshot - .findDynamicSynchronizerParameters() - .futureValueUS - .value - .parameters shouldBe defaultDynamicSynchronizerParameters - - topologyLookup - .maybeOfflineApproximateSnapshot(otherPsid) - .futureValueUS - .left - .value shouldBe ParticipantTopologyManagerError.IdentityManagerParentError( - TopologyManagerError.TopologyStoreUnknown.Failure(SynchronizerStore(otherPsid)) - ) - } - } -} diff --git a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto index ad3e72ffeb..7136f12a5e 100644 --- a/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto +++ b/canton/community/synchronizer/src/main/protobuf/com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto @@ -9,30 +9,11 @@ import "com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v3 import "com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v31/bft_ordering_sequencing_parameters.proto"; service SequencerBftAdministrationService { - // Add a peer endpoint to the list of known endpoints. The sequencer will attempt to connect to the endpoint and - // authenticate it. rpc AddPeerEndpoint(AddPeerEndpointRequest) returns (AddPeerEndpointResponse); - - // Remove a peer endpoint from the list of known endpoints. The sequencer will disconnect from the endpoint if it is - // currently connected. rpc RemovePeerEndpoint(RemovePeerEndpointRequest) returns (RemovePeerEndpointResponse); - - // List the peer endpoints that have been configured and stored in DB. - rpc ListConfiguredEndpoints(ListConfiguredEndpointsRequest) returns (ListConfiguredEndpointsResponse); - - // Get the status of the peer network for the current node; if no endpoints are provided as arguments, - // it returns the global P2P network status, including both outgoing and incoming connections. rpc GetPeerNetworkStatus(GetPeerNetworkStatusRequest) returns (GetPeerNetworkStatusResponse); - - // Get the write readiness of the current node, which indicates whether the node is ready to accept writes - // based on its peer connections and quorum requirements. rpc GetWriteReadiness(GetWriteReadinessRequest) returns (GetWriteReadinessResponse); - - // Get the current ordering topology, including the active sequencer IDs, sequencing parameters, - // and leader/blacklisted sequencer IDs. rpc GetOrderingTopology(GetOrderingTopologyRequest) returns (GetOrderingTopologyResponse); - - // Enable or disable the collection of performance metrics on the sequencer node. rpc SetPerformanceMetricsEnabled(SetPerformanceMetricsEnabledRequest) returns (SetPerformanceMetricsEnabledResponse); } @@ -44,12 +25,6 @@ message RemovePeerEndpointRequest { PeerEndpointId endpoint_id = 1; } -message ListConfiguredEndpointsRequest {} - -message ListConfiguredEndpointsResponse { - repeated PeerEndpoint endpoints = 1; -} - message PeerEndpoint { string address = 1; uint32 port = 2; @@ -90,22 +65,22 @@ message Authenticated { string sequencer_id = 1; } -message GetPeerNetworkStatusRequest { - // If empty, the status of all known endpoints will be returned - repeated PeerEndpointId endpoint_ids = 1; -} +message PeerEndpointHealthStatus { + oneof status { + UnknownEndpoint unknown_endpoint = 1; + Disconnected disconnected = 2; + Unauthenticated unauthenticated = 3; + Authenticated authenticated = 4; + } -message GetPeerNetworkStatusResponse { - repeated PeerConnectionStatus statuses = 1; + message UnknownEndpoint {} + message Disconnected {} + message Unauthenticated {} } -message PeerConnectionStatus { - oneof status { - // An outgoing or incoming connection for which the endpoint is known - PeerEndpointStatus peer_endpoint_status = 1; - // An incoming connection for which the endpoint is not known - Authenticated peer_incoming_connection = 2; - } +message PeerEndpointHealth { + PeerEndpointHealthStatus status = 1; + optional string description = 2; } message PeerEndpointStatus { @@ -114,26 +89,23 @@ message PeerEndpointStatus { PeerEndpointHealth health = 3; } -message PeerEndpointHealth { - PeerEndpointHealthStatus status = 1; - optional string description = 2; -} - -message PeerEndpointHealthStatus { +message PeerConnectionStatus { oneof status { - UnknownEndpoint unknown_endpoint = 1; - Disconnected disconnected = 2; - Unauthenticated unauthenticated = 3; - Authenticated authenticated = 4; + PeerEndpointStatus peer_endpoint_status = 1; + Authenticated peer_incoming_connection = 2; } +} - message UnknownEndpoint {} - message Disconnected {} - message Unauthenticated {} +message GetPeerNetworkStatusRequest { + // If empty, the status of all known endpoints will be returned + repeated PeerEndpointId endpoint_ids = 1; } -message GetWriteReadinessRequest {} +message GetPeerNetworkStatusResponse { + repeated PeerConnectionStatus statuses = 1; +} +message GetWriteReadinessRequest {} message GetWriteReadinessResponse { message P2P { int32 authenticated_peers_count = 1; diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala index 88ccb432d8..89317d19a3 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/block/update/BlockChunkProcessor.scala @@ -350,7 +350,7 @@ final class BlockChunkProcessor( // assigned a sequencing time that corresponds to an actual (i.e. `Send`) event and that is also surely // at or after the acknowledged timestamp. This has no effect whatsoever on transaction processing. chunk.forgetNE.foldLeft[ - (CantonTimestamp, Seq[(CantonTimestamp, Traced[LedgerBlockEvent])]) + (CantonTimestamp, Seq[(CantonTimestamp, Traced[LedgerBlockEvent])]), ]((state.lastChunkTs, Seq.empty)) { case ((lastTs, events), event) => event.value match { case send: Send => @@ -590,7 +590,8 @@ final class BlockChunkProcessor( // Intentionally use the previous block's last timestamp // such that the criterion does not depend on how the block events are chunked up. - tracedSignedAck.value.content.timestamp <= state.lastBlockTs || allowFutureAcksAfterSynchronizerUpgrade + tracedSignedAck.value.content.timestamp <= state.lastBlockTs + || allowFutureAcksAfterSynchronizerUpgrade } invalidTsAcks = futureAcks.map(_.withTraceContext { implicit traceContext => signedAck => val ack = signedAck.content diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala index e7765ca887..2339a78fbe 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/BlockSequencerThroughputCap.scala @@ -163,7 +163,7 @@ class BlockSequencerThroughputCap( @VisibleForTesting private[block] def addBlockUpdateInternal( submissions: Seq[SubmissionRequestEntry] - ): Unit = if (enabled.get()) (lock.exclusive { + ): Unit = if (enabled.get())(lock.exclusive { cancellable.foreach(_.cancel().discard) submissions.foreach { submission => perMessageTypeCaps diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala index 8ab2a899a9..ae7483aa79 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminService.scala @@ -24,7 +24,6 @@ import SequencerBftAdminData.{ WriteReadiness, endpointFromProto, endpointIdFromProto, - endpointToProto, } final class BftOrderingSequencerAdminService( @@ -37,7 +36,6 @@ final class BftOrderingSequencerAdminService( createNetworkStatusPromise: () => Promise[PeerNetworkStatus] = () => Promise(), createOrderingTopologyPromise: () => Promise[Consensus.Admin.GetOrderingTopologyResponse] = () => Promise(), - createPeerEndpointSeqPromise: () => Promise[Seq[P2PEndpoint]] = () => Promise(), )(implicit executionContext: ExecutionContext, metricsContext: MetricsContext) extends SequencerBftAdministrationService with NamedLogging { @@ -82,18 +80,6 @@ final class BftOrderingSequencerAdminService( resultPromise.future.map(RemovePeerEndpointResponse(_)) } - override def listConfiguredEndpoints( - request: ListConfiguredEndpointsRequest - ): Future[ListConfiguredEndpointsResponse] = { - val resultPromise = createPeerEndpointSeqPromise() - p2pNetworkOutAdminRef.asyncSend( - P2PNetworkOut.Admin.ListConfiguredEndpoints(resultPromise.success) - ) - resultPromise.future.map(endpointSeq => - ListConfiguredEndpointsResponse(endpointSeq.map(endpointToProto)) - ) - } - override def getPeerNetworkStatus( request: GetPeerNetworkStatusRequest ): Future[GetPeerNetworkStatusResponse] = { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala index 8c1f7fdc9b..a55f9709eb 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/canton/sequencing/BftBlockOrderer.scala @@ -375,7 +375,7 @@ final class BftBlockOrderer( for { size <- if (overwrite) store.clearAllEndpoints().map(_ => 0) - else store.listEndpoints().map(_.size) + else store.listEndpoints.map(_.size) _ <- if (size == 0) PekkoFutureUnlessShutdown.sequence( diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala index 7940d9e8cf..51e4e84f13 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionManager.scala @@ -1549,12 +1549,11 @@ private[bftordering] object P2PGrpcConnectionManager { case oldState @ P2POutgoingConnectionStatus.Connecting => // Let the gRPC channel setup logic orderly abort the connection attempt - State( - UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) - ) -> ResultWithLogs( - Left(FutureUnlessShutdown.unit), - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), - ) + State(UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId))) + -> ResultWithLogs( + Left(FutureUnlessShutdown.unit), + Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), + ) case oldState @ P2POutgoingConnectionStatus.ConnectingOnChannel( ch, @@ -1572,12 +1571,11 @@ private[bftordering] object P2PGrpcConnectionManager { case oldState @ P2POutgoingConnectionStatus.ConnectingOnChannel(ch, acO, None) => // Let the connect worker orderly abort the connection attempt - State( - UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId)) - ) -> ResultWithLogs( - Right(ch -> acO), - Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), - ) + State(UnlessShutdown.Outcome(p2pConnectionsStatus.removed(p2pEndpointId))) + -> ResultWithLogs( + Right(ch -> acO), + Level.DEBUG -> (() => s"$oldState -> Disconnected (not in state)"), + ) case oldState @ P2POutgoingConnectionStatus.DisconnectingFromChannel(_, _, cw) => // Let the connect worker finish aborting the connection attempt diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala index a4d8d1d377..bd51d31b3d 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/bindings/p2p/grpc/P2PGrpcConnectionState.scala @@ -611,7 +611,7 @@ object P2PGrpcConnectionState { ( this, updatedState, - true + true, ) } { _ => this -> @@ -638,7 +638,7 @@ object P2PGrpcConnectionState { updatedState -> ( this, updatedState, - true + true, ) } { _ => this -> (this, this, false) diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/SegmentState.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/SegmentState.scala index 58d844089d..30bb533012 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/SegmentState.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/consensus/iss/SegmentState.scala @@ -200,10 +200,10 @@ class SegmentState( def commitVotes: Map[BftNodeId, Long] = sumOverInProgressBlocks(_.commitVoters) def discardedMessageCount: Int = - discardedViewMessagesCount + - discardedRetransmittedCommitCertsCount + - segmentBlocks.forgetNE.map(_.discardedMessages).sum + - viewChangeState.values.map(_.discardedMessages).sum + discardedViewMessagesCount + + discardedRetransmittedCommitCertsCount + + segmentBlocks.forgetNE.map(_.discardedMessages).sum + + viewChangeState.values.map(_.discardedMessages).sum private[iss] def retransmittedMessages = retransmittedMessagesCount private[iss] def retransmittedCommitCertificates = retransmittedCommitCertificatesCount diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala index d3289dbeb7..28695b477a 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModule.scala @@ -105,7 +105,7 @@ final class P2PNetworkOutModule[ message match { case P2PNetworkOut.Start => val p2pEndpoints = - context.blockingAwait(p2pEndpointsStore.listEndpoints(), blockingDbReadTimeout) + context.blockingAwait(p2pEndpointsStore.listEndpoints, blockingDbReadTimeout) connectInitialNodes(p2pEndpoints) startModulesIfNeeded() @@ -212,7 +212,6 @@ final class P2PNetworkOutModule[ admin: P2PNetworkOut.Admin )(implicit context: E#ActorContextT[P2PNetworkOut.Message], traceContext: TraceContext): Unit = admin match { - case Admin.AddEndpoint(p2pEndpoint, callback) => if (p2pConnectionState.isDefined(p2pEndpoint.id)) { logger.info(s"Operator requested adding P2P endpoint $p2pEndpoint but it already exists") @@ -230,7 +229,6 @@ final class P2PNetworkOutModule[ abort(s"Failed to P2P add endpoint $p2pEndpoint", exception) } } - case Admin.RemoveEndpoint(p2pEndpointId, callback) => if (p2pConnectionState.isDefined(p2pEndpointId)) { logger.info(s"Removing existing P2P endpoint $p2pEndpointId as requested by operator") @@ -250,16 +248,6 @@ final class P2PNetworkOutModule[ ) callback(false) } - - case Admin.ListConfiguredEndpoints(callback) => - context.pipeToSelf(p2pEndpointsStore.listEndpoints()) { - case Success(endpoints) => - callback(endpoints.sortBy(_.id)) // For output determinism and easier testing - None - case Failure(exception) => - abort(s"Failed to list P2P endpoints", exception) - } - case Admin.GetStatus(callback, p2pEndpointIds) => logger.info( s"Operator requested P2P status for endpoints ${p2pEndpointIds.getOrElse("")}" diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStore.scala index 95a9cbc1ba..13eb895657 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStore.scala @@ -17,7 +17,7 @@ import scala.concurrent.ExecutionContext trait P2PEndpointsStore[E <: Env[E]] extends AutoCloseable { - def listEndpoints()(implicit + def listEndpoints(implicit traceContext: TraceContext ): E#FutureUnlessShutdownT[Seq[P2PEndpoint]] protected final val listEndpointsActionName: String = "list BFT ordering P2P endpoints" diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/db/DbP2PEndpointsStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/db/DbP2PEndpointsStore.scala index 376ae53ce6..bf220ac97f 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/db/DbP2PEndpointsStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/db/DbP2PEndpointsStore.scala @@ -79,7 +79,7 @@ final class DbP2PEndpointsStore( private val profile = storage.profile - override def listEndpoints()(implicit + override def listEndpoints(implicit traceContext: TraceContext ): PekkoEnv#FutureUnlessShutdownT[Seq[P2PEndpoint]] = queryUnlessShutdown( diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/memory/InMemoryP2PEndpointsStore.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/memory/InMemoryP2PEndpointsStore.scala index c30a61b980..b575f96716 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/memory/InMemoryP2PEndpointsStore.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/memory/InMemoryP2PEndpointsStore.scala @@ -28,7 +28,7 @@ abstract class GenericInMemoryP2PEndpointsStore[E <: Env[E]]( protected def createFuture[A](action: String)(x: () => Try[A]): E#FutureUnlessShutdownT[A] - override final def listEndpoints()(implicit + override final def listEndpoints(implicit traceContext: TraceContext ): E#FutureUnlessShutdownT[Seq[P2PEndpoint]] = lock.exclusive { diff --git a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala index 7c7f281a24..3e1550bdc1 100644 --- a/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala +++ b/canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/modules/P2PNetworkOut.scala @@ -47,7 +47,6 @@ object P2PNetworkOut { p2pEndpointId: P2PEndpoint.Id, callback: Boolean => Unit, ) extends Admin - final case class ListConfiguredEndpoints(callback: Seq[P2PEndpoint] => Unit) extends Admin final case class GetStatus( callback: PeerNetworkStatus => Unit, p2pEndpointIds: Option[Iterable[P2PEndpoint.Id]] = None, diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala index a851eddc57..484a8d733a 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/admin/BftOrderingSequencerAdminServiceTest.scala @@ -10,7 +10,6 @@ import com.digitalasset.canton.sequencer.admin.v30.{ GetOrderingTopologyRequest, GetPeerNetworkStatusRequest, GetWriteReadinessRequest, - ListConfiguredEndpointsRequest, PeerEndpoint, PeerEndpointId, RemovePeerEndpointRequest, @@ -90,7 +89,9 @@ class BftOrderingSequencerAdminServiceTest extends AsyncWordSpec with BftSequenc response.added shouldBe true } } + } + "BftOrderingSequencerAdminService" should { "delegate removePeerEndpoint to the p2p out module" in { val p2PNetworkOutAdminSpy = spy[ModuleRef[P2PNetworkOut.Admin]](fakeIgnoringModule[P2PNetworkOut.Admin]) @@ -132,35 +133,9 @@ class BftOrderingSequencerAdminServiceTest extends AsyncWordSpec with BftSequenc response.removed shouldBe true } } + } - "delegate listConfiguredEndpoints to the p2p out module" in { - val p2PNetworkOutAdminSpy = - spy[ModuleRef[P2PNetworkOut.Admin]](fakeIgnoringModule[P2PNetworkOut.Admin]) - val mempoolAdminSpy = - spy[ModuleRef[Mempool.Admin]](fakeIgnoringModule[Mempool.Admin]) - val consensusAdminSpy = - spy[ModuleRef[Consensus.Admin]](fakeIgnoringModule[Consensus.Admin]) - val resultPromise = Promise[Seq[P2PEndpoint]]() - resultPromise.success(Seq.empty) - val bftOrderingSequencerAdminService = - new BftOrderingSequencerAdminService( - mempoolAdminSpy, - p2PNetworkOutAdminSpy, - consensusAdminSpy, - loggerFactory, - createPeerEndpointSeqPromise = () => resultPromise, - ) - bftOrderingSequencerAdminService - .listConfiguredEndpoints(ListConfiguredEndpointsRequest()) - .map { response => - verify(p2PNetworkOutAdminSpy).asyncSend( - P2PNetworkOut.Admin.ListConfiguredEndpoints(any[Seq[P2PEndpoint] => Unit]) - )(any[TraceContext], any[MetricsContext]) - verifyZeroInteractions(mempoolAdminSpy, consensusAdminSpy) - response.endpoints shouldBe empty - } - } - + "BftOrderingSequencerAdminService" should { "delegate getPeerNetworkStatus to the p2p out module" in { val mempoolAdminSpy = spy[ModuleRef[Mempool.Admin]](fakeIgnoringModule[Mempool.Admin]) @@ -188,7 +163,9 @@ class BftOrderingSequencerAdminServiceTest extends AsyncWordSpec with BftSequenc response.statuses shouldBe empty } } + } + "BftOrderingSequencerAdminService" should { "delegate getOrderingTopology to the consensus module" in { val consensusAdminSpy = spy[ModuleRef[Consensus.Admin]](fakeIgnoringModule[Consensus.Admin]) @@ -216,12 +193,13 @@ class BftOrderingSequencerAdminServiceTest extends AsyncWordSpec with BftSequenc Consensus.Admin .GetOrderingTopology(any[Consensus.Admin.GetOrderingTopologyResponse => Unit]) )(any[TraceContext], any[MetricsContext]) - verifyZeroInteractions(mempoolAdminSpy) - verifyNoMoreInteractions(consensusAdminSpy) + verifyZeroInteractions(mempoolAdminSpy, consensusAdminSpy) response.sequencerIds shouldBe empty } } + } + "BftOrderingSequencerAdminService" should { "delegate getSendServiceReadiness to the mempool module" in { val mempoolAdminSpy = spy[ModuleRef[Mempool.Admin]](fakeIgnoringModule[Mempool.Admin]) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala index d7ed32f792..262ee4ac70 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/P2PNetworkOutModuleTest.scala @@ -370,8 +370,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { context.runPipedMessagesThenVerifyAndReceiveOnModule(module) { message => message shouldBe P2PNetworkOut.Internal.Connect(anotherEndpoint) } - module.p2pEndpointsStore - .listEndpoints() + module.p2pEndpointsStore.listEndpoints .apply() should contain theSameElementsInOrderAs otherInitialEndpoints :+ anotherEndpoint endpointAdded shouldBe true @@ -439,8 +438,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { ) context.runPipedMessages() shouldBe empty - module.p2pEndpointsStore - .listEndpoints() + module.p2pEndpointsStore.listEndpoints .apply() should contain theSameElementsInOrderAs otherInitialEndpoints endpointAdded shouldBe false @@ -477,8 +475,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { val remainingEndpoints = Seq(otherInitialEndpointsTupled._2, otherInitialEndpointsTupled._3) - module.p2pEndpointsStore - .listEndpoints() + module.p2pEndpointsStore.listEndpoints .apply() should contain theSameElementsInOrderAs remainingEndpoints context.extractSelfMessages().foreach(module.receive) // Disconnect endpoint endpointRemoved shouldBe true @@ -542,8 +539,7 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { ) context.runPipedMessages() shouldBe empty - module.p2pEndpointsStore - .listEndpoints() + module.p2pEndpointsStore.listEndpoints .apply() should contain theSameElementsInOrderAs otherInitialEndpoints import state.* @@ -556,28 +552,6 @@ class P2PNetworkOutModuleTest extends AnyWordSpec with BftSequencerBaseTest { } } - "it is queried about configured endpoints" should { - "return them" in { - val mempoolSpy = - spy(fakeIgnoringModule[Mempool.Message]) - val (context, _, module, _) = - setupWithIgnoringDefaultDeps(mempool = mempoolSpy) - - otherInitialEndpoints.foreach(module.p2pEndpointsStore.addEndpoint(_).apply()) - - implicit val ctx: ProgrammableUnitTestContext[P2PNetworkOut.Message] = context - - var endpoints: Option[Seq[P2PEndpoint]] = None - module.receive( - P2PNetworkOut.Admin.ListConfiguredEndpoints(e => endpoints = Some(e)) - ) - - context.runPipedMessages() shouldBe empty - - endpoints should contain(otherInitialEndpoints) - } - } - "it is queried about endpoints status" should { "return it" in { val mempoolSpy = diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStoreTest.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStoreTest.scala index 1c905c393d..1486aefbab 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStoreTest.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/p2p/data/P2PEndpointsStoreTest.scala @@ -28,10 +28,10 @@ trait P2PEndpointsStoreTest extends AsyncWordSpec { for { updated1 <- store.addEndpoint(endpoint1) updated2 <- store.addEndpoint(endpoint1) - list1 <- store.listEndpoints() + list1 <- store.listEndpoints updated3 <- store.addEndpoint(endpoint3) - list2 <- store.listEndpoints() + list2 <- store.listEndpoints } yield { list1 should contain only endpoint1 list2 should contain theSameElementsInOrderAs Seq(endpoint1, endpoint3) @@ -45,11 +45,11 @@ trait P2PEndpointsStoreTest extends AsyncWordSpec { val store = createStore() for { updated1 <- store.addEndpoint(endpoint1) - list1 <- store.listEndpoints() + list1 <- store.listEndpoints updated2 <- store.removeEndpoint(endpoint1.id) - list2 <- store.listEndpoints() + list2 <- store.listEndpoints updated3 <- store.removeEndpoint(endpoint1.id) - list3 <- store.listEndpoints() + list3 <- store.listEndpoints } yield { list1 should contain only endpoint1 list2 should be(empty) @@ -65,11 +65,11 @@ trait P2PEndpointsStoreTest extends AsyncWordSpec { for { updated1 <- store.addEndpoint(endpoint1) updated2 <- store.addEndpoint(endpoint2) - list1 <- store.listEndpoints() + list1 <- store.listEndpoints _ <- store.clearAllEndpoints() - list2 <- store.listEndpoints() + list2 <- store.listEndpoints _ <- store.clearAllEndpoints() - list3 <- store.listEndpoints() + list3 <- store.listEndpoints } yield { list1 should contain theSameElementsInOrderAs Seq[P2PEndpoint](endpoint1, endpoint2) list2 should be(empty) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmark.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmark.scala index 6ce09d8edd..10e5a90d65 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmark.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmark.scala @@ -5,12 +5,15 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.perform import com.codahale.metrics.MetricRegistry import com.daml.logging.{ContextualizedLogger, LoggingContext} +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.config.RequireTypes.NonNegativeInt import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftBenchmark.{ Separator, TxStatus, UuidLength, shutdownExecutorService, } +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftBenchmarkConfig.TransactionSizeAndWeight import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftMetrics.{ failedWriteMeters, pendingReads, @@ -45,28 +48,46 @@ final class BftBenchmark( private val readNodeIndices = config.nodes.zipWithIndex - .filter(_._1.isInstanceOf[BftBenchmarkConfig.ReadNode[?]]) - .map(_._2) + .filter { case (node, _) => node.isInstanceOf[BftBenchmarkConfig.ReadNode[?]] } + .map { case (_, index) => index } .toSet - private val ValueBytes: Int = - config.transactionBytes - UuidLength - Separator.length + private val AdjustedTransactionSizesAndWeights: NonEmpty[Seq[TransactionSizeAndWeight]] = + NonEmpty + .from[Seq[TransactionSizeAndWeight]](config.transactionSizesAndWeights.map { + case BftBenchmarkConfig.TransactionSizeAndWeight(size, weight) => + BftBenchmarkConfig.TransactionSizeAndWeight( + NonNegativeInt + .create(size.unwrap - UuidLength - Separator.length) + .getOrElse( + throw new IllegalArgumentException( + s"Transaction size must be at least ${UuidLength + Separator.length} bytes to accommodate the transaction ID and separator. Invalid size: ${size.unwrap}" + ) + ), + weight, + ) + }) + .getOrElse( + throw new IllegalArgumentException( + "At least one transaction size and weight must be provided" + ) + ) - log.info( - s"Payload values will be $ValueBytes bytes long (${config.transactionBytes} - UUID's length)" - ) + log.info(s"Payloads will be $AdjustedTransactionSizesAndWeights") - private val readNodes = config.nodes.flatMap { - case node: BftBenchmarkConfig.ReadNode[?] => Some(node) - case _ => None - } + private val readNodes = + config.nodes.flatMap { + case node: BftBenchmarkConfig.ReadNode[?] => Some(node) + case _ => None + } - private val writeNodes = config.nodes.flatMap { - case node: BftBenchmarkConfig.WriteNode[?] => Some(node) - case _ => None - } + private val writeNodes = + config.nodes.flatMap { + case node: BftBenchmarkConfig.WriteNode[?] => Some(node) + case _ => None + } - private val bftBinding = bftBindingFactory.create(config) + private val bftBinding = bftBindingFactory.create(AdjustedTransactionSizesAndWeights) def run(): JFuture[Unit] = { val txsToBeRead = new ConcurrentHashMap[String, TxStatus]() diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkConfig.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkConfig.scala index 265ca196c3..d9af6b21b9 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkConfig.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkConfig.scala @@ -3,10 +3,12 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} + import scala.concurrent.duration.Duration final case class BftBenchmarkConfig( - transactionBytes: Int, + transactionSizesAndWeights: Seq[BftBenchmarkConfig.TransactionSizeAndWeight], runDuration: Duration, perNodeWritePeriod: Duration, nodes: Seq[BftBenchmarkConfig.Node], @@ -46,4 +48,8 @@ object BftBenchmarkConfig { override val readPort: Int, ) extends WriteNode[Int] with ReadNode[Int] + + final case class TransactionSizeAndWeight(sizeBytes: NonNegativeInt, weight: PositiveInt) + + final case class TransactionSizesAndWeights(payloads: Seq[TransactionSizeAndWeight]) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkTool.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkTool.scala index b5ec0fa82e..3c60d1f0e5 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkTool.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBenchmarkTool.scala @@ -60,7 +60,7 @@ final class BftBenchmarkTool( logger.info( ConfigWriter[BftBenchmarkConfig] .to(config) - .render(renderOptions.setFormatted(false)) + .render(renderOptions) ) val metrics = new MetricRegistry() @@ -139,7 +139,9 @@ final class BftBenchmarkTool( // This is compatible with Jackson and also preserves the intended metric names order (i.e., insertion order). val unifiedReport: SeqMap[MetricName, AnyVal] = - ListMap.newBuilder.addAll((meterReport ++ histogramReport).sortBy(_._1)).result() + ListMap.newBuilder + .addAll((meterReport ++ histogramReport).sortBy { case (statName, _) => statName }) + .result() val reportString = toJson(unifiedReport.asJava) logger.info(reportString) diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBinding.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBinding.scala index 9b6eb22042..8ee7fe7bca 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBinding.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/BftBinding.scala @@ -3,13 +3,19 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.logging.NamedLogging +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftBenchmarkConfig + import java.io.Closeable import java.util.concurrent.CompletionStage -trait BftBindingFactory { +trait BftBindingFactory extends NamedLogging { type T <: BftBinding - def create(config: BftBenchmarkConfig): T + def create( + transactionSizesAndWeights: NonEmpty[Seq[BftBenchmarkConfig.TransactionSizeAndWeight]] + ): T } /** All the operations must be idempotent. diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/GenStandaloneConfig.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/GenStandaloneConfig.scala index 7d0149ea03..3ace434796 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/GenStandaloneConfig.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/GenStandaloneConfig.scala @@ -9,6 +9,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.Bft BftBlockOrderingStandaloneNetworkConfig, BftBlockOrderingStandalonePeerConfig, } +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.SequencingParameters.DefaultSegmentLength import com.digitalasset.canton.topology.{Namespace, SequencerId} import pureconfig.ConfigWriter @@ -59,6 +60,7 @@ object GenStandaloneConfig extends App { thisSequencerId = sequencerId(i), signingPrivateKeyProtoFile = privKeyFile.toJava, signingPublicKeyProtoFile = pubKeyFile.toJava, + segmentLength = DefaultSegmentLength.length.value, peers = (1 to numNodes) .filter(_ != i) .map { j => diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkBinding.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkBinding.scala index 5869117457..a449f09736 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkBinding.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkBinding.scala @@ -3,8 +3,13 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.dabft -import com.daml.logging.{ContextualizedLogger, LoggingContext} +import com.daml.metrics.ExecutorServiceMetrics +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.concurrent.Threading +import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftBenchmark.shutdownExecutorService import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftBinding.TxConsumer import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.{ @@ -19,6 +24,7 @@ import com.digitalasset.canton.synchronizer.sequencing.sequencer.bftordering.sta SendRequest, StandaloneBftOrderingServiceGrpc, } +import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.Mutex import com.google.protobuf.ByteString import io.grpc.inprocess.InProcessChannelBuilder @@ -27,35 +33,58 @@ import io.grpc.{ManagedChannel, ManagedChannelBuilder} import java.util.concurrent.* import scala.concurrent.ExecutionContext -import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.jdk.CollectionConverters.* import scala.language.existentials import scala.util.chaining.* import scala.util.{Failure, Success} -object DaBftBindingFactory extends BftBindingFactory { +class DaBftBindingFactory(override val loggerFactory: NamedLoggerFactory) + extends BftBindingFactory { override type T = DaBftBinding - override def create(config: BftBenchmarkConfig): DaBftBinding = { + override def create( + transactionSizesAndWeights: NonEmpty[Seq[BftBenchmarkConfig.TransactionSizeAndWeight]] + ): DaBftBinding = { + // We could generate enough random payloads in advance, but we may OOM, e.g. with 500k TPS: // // 1m run + 1m margin = 120s, Max total writes = 500k TPS * 120s = 60_000_000 < 2_147_483_647 // 20KB * 60mil ~= 20GB * 60 // // Alternatively, we could generate batches as we go but, since DABFT doesn't - // compress, we just use the same payload every time. - val payloadOutput = ByteString.newOutput(config.transactionBytes) - for (_ <- 0 until config.transactionBytes) - payloadOutput.write('0') - new DaBftBinding(payloadOutput.toByteString) + // compress, we just optimize and use pre-built payloads. + + val payloadsAndWeights = + transactionSizesAndWeights.map { + case BftBenchmarkConfig.TransactionSizeAndWeight(size, weight) => + ByteString.copyFromUtf8("0".repeat(size.unwrap)) -> weight + } + new DaBftBinding(payloadsAndWeights, loggerFactory) } } -final class DaBftBinding(payload: ByteString) extends BftBinding { +final class DaBftBinding( + payloadsAndWeights: NonEmpty[Seq[(ByteString, PositiveInt)]], + override val loggerFactory: NamedLoggerFactory, +) extends BftBinding + with NamedLogging { + + implicit private val traceContext: TraceContext = TraceContext.empty private val MaxReadGrpcBytes = 32 * 1024 * 1024 - private val log = ContextualizedLogger.get(getClass) - implicit private val loggingContext: LoggingContext = LoggingContext.empty + private val MaxRandom = payloadsAndWeights.map { case (_, weight) => weight.unwrap }.sum + + @SuppressWarnings(Array("org.wartremover.warts.Var")) + private val navigablePayloadsWithSizesByCumulativeWeights = { + val navigableMap = new java.util.TreeMap[Int, (ByteString, Int)]() + var cumulativeWeight = 0 + payloadsAndWeights.foreach { case (payload, weight) => + cumulativeWeight += weight.unwrap + navigableMap.put(cumulativeWeight, payload -> payload.size()) + } + navigableMap + } private val writeChannels = new ConcurrentHashMap[ @@ -68,12 +97,28 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { ? ], (ManagedChannel, StreamObserver[ReadOrderedResponse])] - private val scalaFutureExecutor = Executors.newWorkStealingPool() + private val scalaFutureExecutor = + Threading.newExecutionContext( + "da-bft-binding-scala-future", + noTracingLogger, + new ExecutorServiceMetrics(NoOpMetricsFactory), + ) private val scalaFutureExecutionContext = ExecutionContext.fromExecutor(scalaFutureExecutor) - private val readExecutor = Executors.newCachedThreadPool() - private val writeExecutor = Executors.newCachedThreadPool() + private val threadFactory = + new ThreadFactory { + override def newThread(r: Runnable): Thread = { + val thread = new Thread(r) + thread.setContextClassLoader(getClass.getClassLoader) + thread.setUncaughtExceptionHandler((t: Thread, e: Throwable) => + logger.error(s"Uncaught exception in thread ${t.getName}", e) + ) + thread + } + } + private val readExecutor = Executors.newCachedThreadPool(threadFactory) + private val writeExecutor = Executors.newCachedThreadPool(threadFactory) override def write( node: BftBenchmarkConfig.WriteNode[?], @@ -82,11 +127,23 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { val result = new CompletableFuture[Unit]() val (stub, lock) = stubAndLockFor(node) + logger.debug(s"Scheduling a write of txId $txId to node $node") + writeExecutor.submit(new Runnable { override def run(): Unit = { + val payloadSelector = ThreadLocalRandom.current().nextInt(1, MaxRandom + 1) + + logger.debug(s"Selecting payload for writing txId $txId to node $node") + + val (payload, payloadSize) = + navigablePayloadsWithSizesByCumulativeWeights + .ceilingEntry(payloadSelector) + .getValue val request = SendRequest(txId, payload) + logger.debug(s"Writing txId $txId to node $node (payload size $payloadSize bytes)") + lock .exclusive { // The writer is not thread-safe @@ -95,17 +152,25 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { .transform { case Success(response) => response.rejectionReason.fold { - log.debug(s"Wrote txId $txId to node $node") + logger.debug(s"Wrote txId $txId to node $node (payload size $payloadSize bytes)") result.complete(()).discard } { reason => val exception = - new RuntimeException(s"Request $txId was rejected by node $node: $reason") - log.error(s"Rejected writing to node $node", exception) + new RuntimeException( + s"Request $txId (payload size $payloadSize bytes) was rejected by node $node: $reason" + ) + logger.error( + s"Rejected writing txId $txId to node $node (payload size $payloadSize bytes)", + exception, + ) result.completeExceptionally(exception).discard } Success(()) case Failure(exception) => - log.error(s"Error writing to node $node", exception) + logger.error( + s"Error writing txId $txId to node $node (payload size $payloadSize bytes)", + exception, + ) result.completeExceptionally(exception).discard Success(()) }(scalaFutureExecutionContext) @@ -139,7 +204,7 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { val reader: StreamObserver[ReadOrderedResponse] = new StreamObserver[ReadOrderedResponse] { override def onNext(response: ReadOrderedResponse): Unit = { - log.debug(s"Read batch of ${response.block.size} requests") + logger.debug(s"Read batch of ${response.block.size} requests") response.block.foreach { o => readExecutor .submit(new Runnable { @@ -148,10 +213,10 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { try { val txId = o.tag result.complete(txId).discard - log.debug(s"Read back UUID $txId") + logger.debug(s"Read back UUID $txId") } catch { case t: Throwable => - log.error("Error while parsing request", t) + logger.error("Error while parsing request", t) } txConsumer(result) } @@ -161,7 +226,7 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { } override def onError(t: Throwable): Unit = { - log.error("Error from server", t) + logger.error("Error from server", t) complete() } @@ -169,7 +234,7 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { complete() def complete(): Unit = { - log.info(s"Write stream for node $node being completed") + logger.info(s"Write stream for node $node being completed") closeGrpcReadChannel(node) readChannels.remove(node).discard } @@ -212,7 +277,7 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { .pipe { case (_, stub, lock) => (stub, lock) } override def close(): Unit = { - writeChannels.values().asScala.map(_._1).foreach(closeGrpcChannel) + writeChannels.values().asScala.map { case (channel, _, _) => channel }.foreach(closeGrpcChannel) shutdownExecutorService(writeExecutor) closeGrpcStreamsAndChannels(readChannels) @@ -239,7 +304,7 @@ final class DaBftBinding(payload: ByteString) extends BftBinding { private def closeGrpcReadChannel[N <: BftBenchmarkConfig.Node]( node: BftBenchmarkConfig.Node ): Unit = - Option(readChannels.get(node)).map(_._1).foreach(closeGrpcChannel) + Option(readChannels.get(node)).map { case (channel, _) => channel }.foreach(closeGrpcChannel) private def closeGrpcChannel[N <: BftBenchmarkConfig.Node](channel: ManagedChannel): Unit = channel.shutdown().awaitTermination(20, TimeUnit.SECONDS).discard diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkTool.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkTool.scala index 5536804785..35a577b5f6 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkTool.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/dabft/DaBftBenchmarkTool.scala @@ -18,5 +18,6 @@ object DaBftBenchmarkTool extends App { .load[BftBenchmarkConfig] .getOrElse(throw new RuntimeException("Invalid configuration")) - new BftBenchmarkTool(DaBftBindingFactory, NamedLoggerFactory.root).run(config) + new BftBenchmarkTool(new DaBftBindingFactory(NamedLoggerFactory.root), NamedLoggerFactory.root) + .run(config) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortCircuitBindingSpec.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortCircuitBindingSpec.scala index a301ddcd1e..51bf087d89 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortCircuitBindingSpec.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortCircuitBindingSpec.scala @@ -21,7 +21,9 @@ class ShortCircuitBindingSpec extends AnyWordSpec with BaseTest with Matchers { .resources("bftbenchmark-shortcircuit.conf") .load[BftBenchmarkConfig] .getOrElse(throw new RuntimeException("Invalid configuration")) - val metricReport = new BftBenchmarkTool(ShortCircuitBindingFactory, loggerFactory).run(config) + val metricReport = + new BftBenchmarkTool(new ShortCircuitBindingFactory(loggerFactory), loggerFactory) + .run(config) val globalWritesCount = metricReport("global.writes.successful.meter.count").asInstanceOf[Long] diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBenchmarkTool.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBenchmarkTool.scala index 2d98964433..50e4e5e13a 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBenchmarkTool.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBenchmarkTool.scala @@ -18,5 +18,8 @@ object ShortcircuitBenchmarkTool extends App { .load[BftBenchmarkConfig] .getOrElse(throw new RuntimeException("Invalid configuration")) - new BftBenchmarkTool(ShortCircuitBindingFactory, NamedLoggerFactory.root).run(config) + new BftBenchmarkTool( + new ShortCircuitBindingFactory(NamedLoggerFactory.root), + NamedLoggerFactory.root, + ).run(config) } diff --git a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBinding.scala b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBinding.scala index a7c48f42f0..e70ca12eed 100644 --- a/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBinding.scala +++ b/canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/performance/shortcircuit/ShortcircuitBinding.scala @@ -3,6 +3,8 @@ package com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.shortcircuit +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.BftBinding.TxConsumer import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performance.{ BftBenchmarkConfig, @@ -13,11 +15,14 @@ import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.performa import java.util.concurrent.{CompletableFuture, ConcurrentHashMap} import scala.jdk.CollectionConverters.ConcurrentMapHasAsScala -object ShortCircuitBindingFactory extends BftBindingFactory { +class ShortCircuitBindingFactory(override val loggerFactory: NamedLoggerFactory) + extends BftBindingFactory { override type T = ShortCircuitBinding - override def create(config: BftBenchmarkConfig): ShortCircuitBinding = + override def create( + transactionSizesAndWeights: NonEmpty[Seq[BftBenchmarkConfig.TransactionSizeAndWeight]] + ): ShortCircuitBinding = new ShortCircuitBinding } diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala index 284a8bc0cc..6ab5508d12 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/BaseTest.scala @@ -53,7 +53,6 @@ import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.{Assertion, *} import org.scalatestplus.scalacheck.CheckerAsserting import org.slf4j.bridge.SLF4JBridgeHandler -import org.slf4j.event.Level import org.typelevel.discipline.Laws import scala.concurrent.duration.* @@ -179,11 +178,6 @@ trait FutureHelpers extends Assertions with ScalaFuturesWithPatience { self => )(implicit ec: ExecutionContext, pos: source.Position): Future[Assertion] = optionTAssertion.getOrElse(fail(s"Unexpected None value")) - implicit def futureUnlessShutdownAssertionOfOptionTAssertion( - optionTAssertion: OptionT[FutureUnlessShutdown, Assertion] - )(implicit ec: ExecutionContext, pos: source.Position): Future[Assertion] = - optionTAssertion.getOrElse(fail(s"Unexpected None value")).failOnShutdown("shutdown") - /** Converts an EitherT into a Future, failing in case of a [[scala.Left$]]. */ def valueOrFail[F[_], A, B](e: EitherT[F, A, B])( clue: String @@ -419,15 +413,6 @@ trait BaseTest throw ex } } - - /** Suppressed failed clue messages to make our log-checker happy when using clues within an eventually. */ - def suppressFailedClues[T](loggerFactory: SuppressingLogger)(expr: => T): T = - loggerFactory.assertEventuallyLogsSeq(SuppressionRule.Level(Level.ERROR))( - expr, - logEntries => - forAll(logEntries)(logEntry => logEntry.message should startWith("Failed: clue")), - ) - def clueFUS[T]( message: String )(expr: => FutureUnlessShutdown[T])(implicit ec: ExecutionContext): FutureUnlessShutdown[T] = { @@ -469,23 +454,6 @@ trait BaseTest retryOnTestFailuresOnly = retryOnTestFailuresOnly, )(testCode) - /** Keeps evaluating `testCode` until it succeeds or a timeout occurs. - */ - def eventuallySucceeds[T]( - timeUntilSuccess: FiniteDuration = 20.seconds, - maxPollInterval: FiniteDuration = 5.seconds, - suppressErrors: Boolean = true, - )(testCode: => T): T = { - eventually(timeUntilSuccess, maxPollInterval) { - try { - if (suppressErrors) loggerFactory.suppressErrors(testCode) else testCode - } catch { - case e: TestFailedException => throw e - case NonFatal(e) => fail(e) - } - } - } - /** Keeps evaluating `testCode` until it fails or a timeout occurs. * @return * the result the last evaluation of `testCode` @@ -551,8 +519,6 @@ trait BaseTest } object BaseTest { - val DefaultEventuallyTimeUntilSuccess: FiniteDuration = 20.seconds - implicit class RichSynchronizerIdO(val id: SynchronizerId) { def toPhysical: PhysicalSynchronizerId = PhysicalSynchronizerId(id, NonNegativeInt.zero, testedProtocolVersion) @@ -608,8 +574,8 @@ object BaseTest { ) ) def eventually[T]( - timeUntilSuccess: FiniteDuration = DefaultEventuallyTimeUntilSuccess, - maxPollInterval: FiniteDuration = 100.millis, + timeUntilSuccess: FiniteDuration = 20.seconds, + maxPollInterval: FiniteDuration = 5.seconds, retryOnTestFailuresOnly: Boolean = true, )(testCode: => T): T = { require( @@ -617,7 +583,7 @@ object BaseTest { s"The timeout must not be negative, but is $timeUntilSuccess", ) val deadline = timeUntilSuccess.fromNow - var sleepMs = 10L min (maxPollInterval.toMillis / 10L) + var sleepMs = 1L def sleep(): Unit = { val timeLeft = deadline.timeLeft.toMillis max 0 Threading.sleep(sleepMs min timeLeft) diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/LogReporter.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/LogReporter.scala index cfcbb6dfba..140abe8c56 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/LogReporter.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/LogReporter.scala @@ -37,12 +37,7 @@ class LogReporter extends Reporter { case event: TestSucceeded => logger.info(s"Test succeeded: '${event.suiteName}/${event.testName}'") case event: TestFailed => - val locationMsg = event.location.map(l => s", location: $l").getOrElse("") - val msg = - s"Test failed: '${event.suiteName}/${event.testName}', message: ${event.message}$locationMsg" - event.throwable - .map(cause => logger.error(msg, cause)) - .getOrElse(logger.error(msg)) + warnWithThrowable(s"Test failed: '${event.suiteName}/${event.testName}'", event.throwable) case event: TestCanceled => logger.info(s"Test canceled: '${event.suiteName}/${event.testName}'") case event: TestIgnored => logger.info(s"Test ignored: '${event.suiteName}/${event.testName}'") diff --git a/canton/community/testing/src/main/scala/com/digitalasset/canton/store/db/DbStorageSetup.scala b/canton/community/testing/src/main/scala/com/digitalasset/canton/store/db/DbStorageSetup.scala index 1781760a4f..3968e9ead9 100644 --- a/canton/community/testing/src/main/scala/com/digitalasset/canton/store/db/DbStorageSetup.scala +++ b/canton/community/testing/src/main/scala/com/digitalasset/canton/store/db/DbStorageSetup.scala @@ -364,10 +364,9 @@ object DbStorageSetup { val isCI = sys.env.contains("CI") val isMachine = sys.env.contains("MACHINE") - val isGha = sys.env.contains("GITHUB_ACTION") val useTestContainerByForce = sys.env.contains("DB_FORCE_TEST_CONTAINER") || forceTestContainer - if (!useTestContainerByForce && ((isCI && !isMachine) || isGha)) + if (!useTestContainerByForce && (isCI && !isMachine)) new PostgresCISetup(migrationMode, mkDbConfig, useDbNameO, loggerFactory).initialized() else new PostgresTestContainerSetup(migrationMode, mkDbConfig, loggerFactory).initialized() } diff --git a/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/HeaderName.scala b/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/HeaderName.scala deleted file mode 100644 index 889e38e19e..0000000000 --- a/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/HeaderName.scala +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.canton.tracing - -import java.util.Locale - -/** HTTP header name with case-insensitive equality (RFC 7230 ยง3.2). The - * apply factory normalizes to lowercase, so equality and hashing on the - * underlying String are case-insensitive by construction. Use as the key - * type for any map of HTTP headers to avoid lookup misses caused by - * upstream case variation. - */ -final class HeaderName private (val value: String) extends AnyVal { - override def toString: String = value -} - -object HeaderName { - def apply(name: String): HeaderName = - new HeaderName(name.toLowerCase(Locale.ROOT)) -} diff --git a/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/TraceContextGrpc.scala b/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/TraceContextGrpc.scala index 81666ebb56..26330b7183 100644 --- a/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/TraceContextGrpc.scala +++ b/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/TraceContextGrpc.scala @@ -6,7 +6,6 @@ package com.digitalasset.canton.tracing import io.grpc.* import io.grpc.Context as GrpcContext import io.grpc.ForwardingClientCall.SimpleForwardingClientCall -import io.grpc.stub.AbstractStub import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener import io.opentelemetry.api.trace.{Span, Tracer} @@ -18,17 +17,12 @@ import scala.util.{Try, Using} */ object TraceContextGrpc { // value of trace context in the GRPC Context - // There are two options for implicitly propagating the trace context within a process: thread-local storage and - // attaching custom call options to a GRPC call. Thread-local storage does *not* work with Futures, so we recommend - // using the latter approach where possible. The former is used sometimes for historical purposes, and sometimes - // because for technical reasons. - private val TraceContextThreadLocalKey = - Context.keyWithDefault[TraceContext]("TraceContextThreadLocalKey", TraceContext.empty) + private val TraceContextKey = + Context.keyWithDefault[TraceContext]("traceContext", TraceContext.empty) - val TraceContextCallOptionKey = - CallOptions.Key.create[TraceContext]("TraceContextCallOptionKey") + val TraceContextOptionsKey = CallOptions.Key.create[TraceContext]("traceContext") - def fromGrpcContext: TraceContext = TraceContextThreadLocalKey.get() + def fromGrpcContext: TraceContext = TraceContextKey.get() def fromGrpcContextOrNew(name: String): TraceContext = { val grpcTraceContext = TraceContextGrpc.fromGrpcContext @@ -42,37 +36,11 @@ object TraceContextGrpc { def withGrpcTraceContext[A](f: TraceContext => A): A = f(fromGrpcContext) def withGrpcContext[A](traceContext: TraceContext)(fn: => A): A = { - val context = GrpcContext.current().withValue(TraceContextThreadLocalKey, traceContext) + val context = GrpcContext.current().withValue(TraceContextKey, traceContext) context.call(() => fn) } - def addTraceContextToCallOptions[T <: AbstractStub[T]]( - stub: T - )(implicit traceContext: TraceContext): T = { - stub.withOption(TraceContextCallOptionKey, traceContext) - } - - def inferServerRequestTraceContext(span: String): TraceContext = { - val grpcTraceContext = TraceContextGrpc.fromGrpcContext - if (grpcTraceContext.traceId.isDefined) { - grpcTraceContext - } else { - TraceContext.withNewTraceContext(span)(identity) - } - } - - def inferCallerTraceContext(callOptions: CallOptions): Option[TraceContext] = { - val callOptionTraceContext = callOptions.getOption(TraceContextGrpc.TraceContextCallOptionKey) - if (callOptionTraceContext == null) { - // TODO(#9754): remove the need to infer the trace context from thread-local storage, which doesn't work with Futures in the mix, and log a big fat warning if we do - val grpcTraceContext = TraceContextGrpc.fromGrpcContext - Option.when(grpcTraceContext.traceId.isDefined)(grpcTraceContext) - } else { - Some(callOptionTraceContext) - } - } - private implicit final class TryFailedOps[A](private val a: Try[A]) extends AnyVal { @inline def valueOrThrow: A = a.fold(throw _, identity) @@ -97,8 +65,8 @@ object TraceContextGrpc { callOptions: CallOptions, next: Channel, ): ClientCall[ReqT, RespT] = { - val tcOpts = Option(callOptions.getOption(TraceContextCallOptionKey)) - val traceContext = tcOpts.getOrElse(TraceContextThreadLocalKey.get()) + val tcOpts = Option(callOptions.getOption(TraceContextOptionsKey)) + val traceContext = tcOpts.getOrElse(TraceContextKey.get()) val contextToPropagate = traceContext.context def withPropagatedContext[T](fn: => T): T = @@ -140,7 +108,7 @@ object TraceContextGrpc { val traceContext = W3CTraceContext.fromGrpcMetadata(headers) val context = GrpcContext .current() - .withValue(TraceContextThreadLocalKey, traceContext) + .withValue(TraceContextKey, traceContext) Contexts.interceptCall(context, call, headers, next) } } @@ -167,7 +135,7 @@ object TraceContextGrpc { val context = GrpcContext .current() - .withValue(TraceContextThreadLocalKey, traceContext) + .withValue(TraceContextKey, traceContext) val nextServerCallListener = Contexts.interceptCall(context, call, headers, next) new ServerCallListener(nextServerCallListener, span) diff --git a/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/W3CTraceContext.scala b/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/W3CTraceContext.scala index 09df3c9f6f..711b2e708d 100644 --- a/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/W3CTraceContext.scala +++ b/canton/community/util-observability/src/main/scala/com/digitalasset/canton/tracing/W3CTraceContext.scala @@ -24,8 +24,8 @@ final case class W3CTraceContext(parent: String, state: Option[String] = None) /** HTTP headers of this trace context. Marked transient as the headers do not need to be * serialized when using java serialization. */ - @transient lazy val asHeaders: Map[HeaderName, String] = - Map(TraceparentHeader -> parent) ++ state.map(TracestateHeader -> _).toList + @transient lazy val asHeaders: Map[String, String] = + Map(TRACEPARENT_HEADER_NAME -> parent) ++ state.map(TRACESTATE_HEADER_NAME -> _).toList override def toString: String = { val sb = new mutable.StringBuilder() @@ -41,27 +41,27 @@ final case class W3CTraceContext(parent: String, state: Option[String] = None) object W3CTraceContext { // https://www.w3.org/TR/trace-context/ private val propagator = W3CTraceContextPropagator.getInstance() - // values match W3CTraceContextPropagator.TRACE_PARENT / TRACE_STATE - private val TraceparentHeader = HeaderName("traceparent") - private val TracestateHeader = HeaderName("tracestate") + private val TRACEPARENT_HEADER_NAME = + "traceparent" // same as W3CTraceContextPropagator.TRACE_PARENT + private val TRACESTATE_HEADER_NAME = "tracestate" // same as W3CTraceContextPropagator.TRACE_STATE @SuppressWarnings(Array("org.wartremover.warts.Var")) def fromOpenTelemetryContext(context: OpenTelemetryContext): Option[W3CTraceContext] = { var builder = new W3CTraceContextBuilder val setter: TextMapSetter[W3CTraceContextBuilder] = (carrier, key, value) => - builder = HeaderName(key) match { - case TraceparentHeader => carrier.copy(parent = Some(value)) - case TracestateHeader => carrier.copy(state = Some(value)) + builder = key match { + case TRACEPARENT_HEADER_NAME => carrier.copy(parent = Some(value)) + case TRACESTATE_HEADER_NAME => carrier.copy(state = Some(value)) case _ => carrier } propagator.inject(context, builder, setter) builder.build } - def fromHeaders(headers: Map[HeaderName, String]): Option[W3CTraceContext] = + def fromHeaders(headers: Map[String, String]): Option[W3CTraceContext] = W3CTraceContextBuilder( - headers.get(TraceparentHeader), - headers.get(TracestateHeader), + headers.get(TRACEPARENT_HEADER_NAME), + headers.get(TRACESTATE_HEADER_NAME), ).build private final case class W3CTraceContextBuilder( @@ -96,12 +96,10 @@ object W3CTraceContext { * returned but the current span will be invalid and [[TraceContext.traceId]] will return None. */ @SuppressWarnings(Array("org.wartremover.warts.Null")) - def toTraceContext(parent: Option[String], state: Option[String]): TraceContext = extract { key => - HeaderName(key) match { - case TraceparentHeader => parent.orNull - case TracestateHeader => state.orNull - case _ => null - } + def toTraceContext(parent: Option[String], state: Option[String]): TraceContext = extract { + case `TRACEPARENT_HEADER_NAME` => parent.orNull + case `TRACESTATE_HEADER_NAME` => state.orNull + case _ => null } private def extract(getter: String => String): TraceContext = diff --git a/canton/community/util-observability/src/test/resources/logback-test.xml b/canton/community/util-observability/src/test/resources/logback-test.xml deleted file mode 100644 index eebfeef4c0..0000000000 --- a/canton/community/util-observability/src/test/resources/logback-test.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - true - - - - - - - OFF - - - - - ${LOG_FILE_NAME:-log/canton_network_test.clog} - ${LOG_APPEND:-true} - - - [ignore] - [ignore] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 926766d080892e9bcb41af465d1f30371787ddb8 Mon Sep 17 00:00:00 2001 From: Stephen Compall Date: Tue, 7 Jul 2026 21:23:59 +0000 Subject: [PATCH 02/13] Bump Canton commit Signed-off-by: Stephen Compall --- canton/.proto_snapshot_image.bin.gz | Bin 518672 -> 517511 bytes canton/CODEOWNERS | 3 + canton/UNRELEASED.md | 845 +--------------- canton/VERSION | 2 +- .../src/main/resources/logback-test.xml | 61 ++ .../testing/utils/GrpcServerResource.scala | 7 +- canton/build.sbt | 4 + .../synchronizer_connectivity_service.proto | 34 +- .../canton/admin/topology/v30/common.proto | 20 + .../client/commands/LedgerApiCommands.scala | 62 +- .../commands/ParticipantAdminCommands.scala | 83 +- .../client/data/RegisteredSynchronizer.scala | 107 +++ .../client/data/SequencerConnections.scala | 51 +- .../data/SynchronizerConnectionConfig.scala | 15 +- .../admin/api/client/data/Topology.scala | 29 + .../canton/config/CantonConfig.scala | 250 +++-- .../canton/console/AdminCommandRunner.scala | 4 +- .../canton/console/ConsoleEnvironment.scala | 31 +- .../console/GrpcAdminCommandRunner.scala | 3 +- .../canton/console/HealthDumpGenerator.scala | 4 +- ...ExternalPartiesTestingAdministration.scala | 6 +- .../commands/LedgerApiAdministration.scala | 32 + .../commands/ParticipantAdministration.scala | 29 +- .../declarative/DeclarativeApiManager.scala | 1 + .../DeclarativeParticipantApi.scala | 18 +- .../CommunityEnvironmentFactory.scala | 6 +- .../canton/environment/Environment.scala | 67 +- .../canton/environment/package.scala | 10 + .../main/resources/sandbox/bootstrap.canton | 2 +- .../digitalasset/canton/CantonAppDriver.scala | 29 +- .../canton/CantonCommunityApp.scala | 18 +- .../com/digitalasset/canton/Runner.scala | 14 +- .../digitalasset/canton/config/Generate.scala | 2 +- .../composability-auto-reassignment.canton | 2 +- .../05-composability/composability1.canton | 2 +- .../05-composability/composability2.canton | 2 +- .../requirements.txt | 2 +- .../canton/config/CantonConfigTest.scala | 70 +- .../canton/config/ConfigValidationsTest.scala | 28 +- .../console/ConsoleEnvironmentTest.scala | 8 +- .../canton/console/ConsoleTest.scala | 6 +- .../CommunityEnvironmentFixture.scala | 2 +- .../canton/environment/NodesTest.scala | 2 + ...wnloadTopologyForInitIntegrationTest.scala | 13 +- .../tests/DumpIntegrationTest.scala | 2 + ...MultipleSynchronizersIntegrationTest.scala | 2 +- .../tests/ProtobufCompatibilityTest.scala | 35 +- .../ReplicatedMediatorIntegrationTest.scala | 4 +- .../tests/ReplicatedParticipantTest.scala | 4 +- ...SequencerIdsRetrieverIntegrationTest.scala | 15 +- ...nRequestAmplificationIntegrationTest.scala | 13 +- .../SynchronizerChangeIntegrationTest.scala | 2 +- ...chronizerConnectivityIntegrationTest.scala | 14 + ...nchronizerRouterIntegrationTestSetup.scala | 2 +- .../tests/TickRequestIntegrationTest.scala | 2 +- .../acs/ActiveContractsIntegrationTest.scala | 11 +- ...csCommitmentProcessorIntegrationTest.scala | 2 +- .../AcsCommitmentToolingIntegrationTest.scala | 2 +- .../commitment/util/CommitmentTestUtil.scala | 60 +- .../benchmarks/ReplayingParticipant.scala | 1 + .../bftsynchronizer/ReassignmentTest.scala | 2 +- ...tSequencerConnectionsIntegrationTest.scala | 8 +- .../ConnectionPoolHealthIntegrationTest.scala | 8 +- ...ncerConnectionServiceIntegrationTest.scala | 8 +- .../AcsCommitmentCrashIntegrationTest.scala | 7 +- .../MediatorFailoverIntegrationTest.scala | 4 +- .../ParticipantRestartTest.scala | 15 +- .../crashrecovery/SequencerRestartTest.scala | 2 +- .../examples/ExampleIntegrationTest.scala | 8 +- .../MultisyncExampleIntegrationTest.scala | 4 +- .../health/RemoteDumpIntegrationTest.scala | 6 +- .../ledgerapi/AchsInitIntegrationTest.scala | 162 ++++ .../tests/ledgerapi/AchsIntegrationTest.scala | 4 +- .../ledgerapi/AchsRepairIntegrationTest.scala | 4 +- .../LedgerApiParticipantPruningTest.scala | 121 ++- .../ledgerapi/auth/GetCompletionsAuthIT.scala | 118 +++ .../auth/IDPBoxingServiceCallOutTests.scala | 40 + .../ledgerapi/fixture/CantonFixture.scala | 4 +- ...ternalPartyOnboardingIntegrationTest.scala | 182 +++- ...eSubmissionVersioningIntegrationTest.scala | 3 +- .../tests/manual/BftOrderingBenchmark.scala | 93 +- .../manual/BftSingleNodePerformanceTest.scala | 7 +- .../tests/manual/DataContinuityTest.scala | 61 +- .../tests/manual/S3Synchronization.scala | 2 +- .../topology/TopologyStateVerification.scala | 1 + ...ConnectionPoolMetricsIntegrationTest.scala | 232 +++++ ...entDecentralizedPartyIntegrationTest.scala | 2 +- .../DivulgenceIntegrationTestHelpers.scala | 4 +- ...tyReplicationParticipantProtocolTest.scala | 2 +- .../offpr/DivulgenceIntegrationTest.scala | 21 +- .../offpr/WorkflowIdsIntegrationTest.scala | 10 +- ...entBeforeUnassignmentIntegrationTest.scala | 2 +- ...sReassignmentProtocolIntegrationTest.scala | 2 +- ...cReassignmentBatchingIntegrationTest.scala | 2 +- ...onfirmationAdminPartyIntegrationTest.scala | 2 +- ...tConfirmationPoliciesIntegrationTest.scala | 2 +- ...entNoReassignmentDataIntegrationTest.scala | 2 +- ...ncurrentReassignmentsIntegrationTest.scala | 2 +- .../ReassignmentServiceIntegrationTest.scala | 2 +- ...imeoutCommandRejectedIntegrationTest.scala | 2 +- ...eassignmentSubmissionIntegrationTest.scala | 2 +- ...gnmentTargetTimestampIntegrationTest.scala | 2 +- ...ConfirmationObserversIntegrationTest.scala | 2 +- ...ConfirmationThresholdIntegrationTest.scala | 2 +- .../RepairServiceIntegrationTest.scala | 2 +- .../UpdateServiceIntegrationTest.scala | 2 +- ...ryptoNoPreDefinedKeysIntegrationTest.scala | 4 +- ...cerCatchUpPerformanceIntegrationTest.scala | 77 +- .../operations/ViewConsistencyTest.scala | 4 +- .../tests/pkgdars/PackageUsableMixin.scala | 4 +- .../LedgerPruningIntegrationTest.scala | 16 +- .../ReassignmentPruningIntegrationTest.scala | 2 +- ...rtReassignmentCounterIntegrationTest.scala | 4 +- .../ImportContractsIntegrationTest.scala | 24 +- .../repair/RepairServiceIntegrationTest.scala | 16 +- .../RollbackUnassignmentIntegrationTest.scala | 2 +- .../CryptoMetricsIntegrationTest.scala | 58 ++ ...alidAssignmentRequestIntegrationTest.scala | 2 +- ...InvalidReassignmentIdIntegrationTest.scala | 2 +- ...idUnassignmentRequestIntegrationTest.scala | 2 +- .../KeyManagementIntegrationTest.scala | 2 +- .../kms/KmsCryptoIntegrationTestBase.scala | 2 +- ...ptoWithPreDefinedKeysIntegrationTest.scala | 4 +- .../SessionSigningKeysIntegrationTest.scala | 11 +- ...nSigningKeysLifecycleIntegrationTest.scala | 10 +- ...sEncryptedCryptoPrivateStoreTestBase.scala | 4 +- .../aws/AwsKmsCryptoIntegrationTestBase.scala | 4 +- ...pEncryptedCryptoPrivateStoreTestBase.scala | 4 +- .../gcp/GcpKmsCryptoIntegrationTestBase.scala | 4 +- ...kEncryptedCryptoPrivateStoreTestBase.scala | 4 +- ...ckKmsDriverCryptoIntegrationTestBase.scala | 4 +- .../PackageRemovalIntegrationTest.scala | 2 +- .../PackageVettingIntegrationTest.scala | 83 +- .../DynamicOnboardingIntegrationTest.scala | 11 + .../sequencer/ToxiproxyIntegrationTest.scala | 11 +- ...cerWithTrafficControlApiTestPostgres.scala | 2 + .../bftordering/BftSequencerApiTest.scala | 2 + .../reference/ReferenceSequencerApiTest.scala | 2 + ...quencerWithTrafficControlApiTestBase.scala | 3 + ...cerWithTrafficControlApiTestPostgres.scala | 2 + ...artyToParticipantAuthIntegrationTest.scala | 12 +- ...tionMultiSynchronizerIntegrationTest.scala | 2 +- ...tSequencerConnectionsIntegrationTest.scala | 8 +- .../ParticipantTrafficEnforcementTest.scala | 213 +++++ .../tests/traffic/TrafficControlTest.scala | 3 + ...nNetworkTopologyStateIntegrationTest.scala | 4 +- .../lsu/LsuCommandIdIntegrationTest.scala | 76 +- .../LsuEarlyHandshakeIntegrationTest.scala | 2 +- .../lsu/LsuEndToEndIntegrationTest.scala | 94 ++ ...suExhaustiveHandshakeIntegrationTest.scala | 206 ++++ ...rectSequencerIdentityIntegrationTest.scala | 21 + .../lsu/LsuMetricsIntegrationTest.scala | 2 +- ...flinePartyReplicationIntegrationTest.scala | 23 +- ...suPurgeStoresAfterLsuIntegrationTest.scala | 21 +- .../lsu/LsuReassignmentsIntegrationTest.scala | 2 +- ...SuccessorSynchronizerIntegrationTest.scala | 55 +- ...encerContactSuccessorIntegrationTest.scala | 141 ++- ...LsuSessionSigningKeysIntegrationTest.scala | 2 +- ...rafficTransferRestartIntegrationTest.scala | 12 +- ...olVersionReassignmentIntegrationTest.scala | 2 +- .../util/PartyToParticipantDeclarative.scala | 16 +- .../util/TestSubmissionService.scala | 19 +- .../canton/util/ReleaseUtils.scala | 4 + .../canton/protocol/v30/topology.proto | 2 +- .../canton/topology/admin/v30/common.proto | 12 + .../canton/config/AuthServiceConfig.scala | 3 + .../canton/config/BaseCantonConfig.scala | 6 + .../config/SessionSigningKeysConfig.scala | 2 +- .../canton/config/StorageConfig.scala | 8 + .../canton/config/TestingConfigInternal.scala | 7 +- .../canton/crypto/CryptoApi.scala | 11 + .../canton/crypto/Encryption.scala | 72 +- .../digitalasset/canton/crypto/Signing.scala | 68 +- .../SyncCryptoApiParticipantProvider.scala | 10 +- .../crypto/SynchronizerCryptoPrivateApi.scala | 7 +- .../crypto/SynchronizerCryptoPureApi.scala | 8 +- .../crypto/provider/jce/JceCrypto.scala | 6 + .../provider/jce/JcePrivateCrypto.scala | 3 + .../crypto/provider/jce/JcePureCrypto.scala | 8 +- .../provider/kms/KmsPrivateCrypto.scala | 10 +- .../crypto/signer/SyncCryptoSigner.scala | 6 +- .../SyncCryptoSignerWithSessionKeys.scala | 17 +- .../crypto/verifier/SyncCryptoVerifier.scala | 16 + .../canton/metrics/CryptoMetrics.scala | 19 + .../canton/metrics/DecryptionMetrics.scala | 31 + .../SequencerConnectionPoolMetrics.scala | 28 +- .../canton/metrics/SigningMetrics.scala | 31 + .../canton/networking/grpc/GrpcError.scala | 28 +- .../canton/resource/DbMigrations.scala | 11 +- .../canton/resource/DbStorageMulti.scala | 2 +- .../canton/resource/DbStorageSingle.scala | 2 +- .../client/PeriodicAcknowledgements.scala | 13 +- .../sequencing/client/SequencerClient.scala | 32 +- .../client/SequencerClientFactory.scala | 12 +- .../client/SequencerClientSend.scala | 9 + .../GrpcInternalSequencerConnection.scala | 2 +- .../client/pool/GrpcSequencerConnection.scala | 18 +- .../GrpcUserSequencerConnectionStub.scala | 5 +- .../client/pool/SequencerConnectionPool.scala | 4 + .../pool/SequencerConnectionPoolImpl.scala | 16 +- .../pool/SequencerSubscriptionPoolImpl.scala | 3 +- .../GrpcSubscriptionErrorRetryPolicy.scala | 3 + .../sequencing/protocol/ClosedEnvelope.scala | 14 + .../protocol/ClosedUncompressedEnvelope.scala | 11 +- .../sequencing/protocol/SendAsyncError.scala | 3 + .../protocol/SequencerDeliverError.scala | 23 + .../sequencing/protocol/SignedContent.scala | 13 + .../digitalasset/canton/store/Purgeable.scala | 2 + .../com/digitalasset/canton/time/Clock.scala | 76 +- .../canton/time/SynchronizerTimeTracker.scala | 13 +- .../ConfiguredPhysicalSynchronizerId.scala | 1 + .../canton/topology/TopologyManager.scala | 32 +- .../TopologyManagerSigningKeyDetection.scala | 21 +- .../canton/topology/store/TopologyStore.scala | 38 +- .../topology/store/db/DbTopologyStore.scala | 4 +- .../transaction/TopologyMapping.scala | 10 +- .../digitalasset/canton/util/PekkoUtil.scala | 47 +- .../ShardedSequentialProcessingQueue.scala | 174 ++++ .../canton/util/SimpleExecutionQueue.scala | 30 +- .../util/TracedPossiblyPrevalidated.scala | 31 + .../canton/h2/dev/V5_3__tea_initial.sha256 | 1 + .../canton/h2/dev/V5_3__tea_initial.sql | 75 ++ .../h2/stable/V5_2__dabft_partitioning.sha256 | 1 + .../h2/stable/V5_2__dabft_partitioning.sql | 8 + .../postgres/dev/V5_3__tea_initial.sha256 | 1 + .../canton/postgres/dev/V5_3__tea_initial.sql | 75 ++ ..._optimize_contract_key_lookup_index.sha256 | 1 + ..._1__optimize_contract_key_lookup_index.sql | 6 + .../stable/V5_2__dabft_partitioning.sha256 | 1 + .../stable/V5_2__dabft_partitioning.sql | 293 ++++++ ..._auto_vacuum_analyze_table_settings.sha256 | 2 +- ...ult_auto_vacuum_analyze_table_settings.sql | 62 -- .../environment/CantonNodeBootstrap.scala | 5 +- .../grpc/GrpcTopologyManagerReadService.scala | 44 +- .../GrpcTopologyManagerWriteService.scala | 3 +- .../canton/util/GrpcStreamingUtils.scala | 247 ++++- .../canton/crypto/kms/KmsTest.scala | 2 + .../crypto/provider/jce/JceCryptoTest.scala | 2 + .../crypto/provider/kms/KmsCryptoTest.scala | 2 + .../canton/crypto/sync/SyncCryptoTest.scala | 2 + .../sync/SyncCryptoWithLongTermKeysTest.scala | 2 +- .../sync/SyncCryptoWithSessionKeysTest.scala | 4 +- .../SyncSchemeValidationsTest.scala | 2 + .../client/SequencerClientTest.scala | 4 +- .../client/TestSequencerClientSend.scala | 2 + .../pool/ConnectionPoolTestHelpers.scala | 11 +- .../SequencerConnectionPoolImplTest.scala | 57 +- .../digitalasset/canton/store/db/DbTest.scala | 2 +- ...pologyManagerSigningKeyDetectionTest.scala | 101 +- ...ogyStateForInitializationServiceTest.scala | 7 +- .../topology/store/TopologyStoreTest.scala | 7 +- .../store/TopologyStoreTestData.scala | 8 +- ...rafficPurchasedSubmissionHandlerTest.scala | 6 + .../canton/util/GrpcStreamingUtilsTest.scala | 371 +++++++- .../canton/util/PekkoUtilTest.scala | 721 ++++++++------ .../util/SimpleExecutionQueueTest.scala | 1 + .../ProtocolContinuityConformanceTest.scala | 17 +- .../tests/ledgerapi/ExcludedTests.scala | 11 +- .../JsonApiConformanceIntegrationTest.scala | 13 +- .../ledgerapi/LedgerApiConformanceTest.scala | 42 +- .../LedgerApiVariationsConformanceTest.scala | 12 +- .../digitalasset/daml/lf/engine/Engine.scala | 39 +- .../digitalasset/daml/lf/engine/Result.scala | 81 +- .../UnsupportedContractIdEngineSpec.scala | 128 +++ .../integration/tests/UpgradesMatrixIT.scala | 52 +- .../daml/lf/engine/UpgradesMatrix.scala | 397 ++++++-- .../daml/lf/engine/UpgradesMatrixUnit.scala | 87 +- .../daml/lf/validation/Typing.scala | 16 +- .../integration/tests/DamlScriptIT.scala | 4 +- .../BaseEnvironmentDefinition.scala | 48 + .../integration/BaseIntegrationTest.scala | 14 +- .../CommunityIntegrationTest.scala | 9 +- .../canton/integration/ConfigTransforms.scala | 2 +- .../integration/EnvironmentDefinition.scala | 35 +- .../canton/integration/EnvironmentSetup.scala | 65 +- .../integration/EnvironmentSetupPlugin.scala | 14 +- .../canton/integration/HasCycleUtils.scala | 202 ++++ .../integration/HasTrailingNoneUtils.scala | 47 + .../LedgerApiStoreIntegrityChecker.scala | 6 +- .../canton/integration/TestEnvironment.scala | 9 +- .../bootstrap/NetworkBootstrapper.scala | 30 +- .../canton/integration/package.scala | 16 +- .../plugins/UseLedgerApiTestTool.scala | 437 +++++++++ .../util/MultiSynchronizerFeatureFlag.scala | 14 +- .../ProgrammableSequencerPolicies.scala | 2 +- .../testing/v1/KmsDriverTestUtils.scala | 5 + .../src/main/resources/logback.xml | 23 + .../com/daml/ledger/api/testtool/Main.scala | 2 +- .../infrastructure/LedgerServices.scala | 40 +- .../infrastructure/LedgerTestSuite.scala | 3 + .../participant/ParticipantTestContext.scala | 20 + .../SingleParticipantTestContext.scala | 38 + .../TimeoutParticipantTestContext.scala | 27 + .../api/testtool/runner/AvailableTests.scala | 7 + .../ledger/api/testtool/suites/V2_2.scala | 51 +- .../ledger/api/testtool/suites/V2_3.scala | 12 +- .../ledger/api/testtool/suites/V2_dev.scala | 2 +- .../v2_2/ActiveContractsServiceIT.scala | 59 +- .../v2_2/CheckpointInTailingStreamsIT.scala | 5 +- ...mmandDeduplicationPeriodValidationIT.scala | 5 +- .../suites/v2_2/CommandServiceIT.scala | 13 +- .../v2_2/CommandSubmissionCompletionIT.scala | 5 +- .../v2_2/CommandSubmissionTestUtils.scala | 9 +- .../suites/v2_2/CompanionImplicits.scala | 106 --- .../suites/v2_2/ContractServiceIT.scala | 5 +- .../testtool/suites/v2_2/DivulgenceIT.scala | 5 +- .../suites/v2_2/EventQueryServiceIT.scala | 5 +- .../suites/v2_2/ExplicitDisclosureIT.scala | 87 +- .../suites/v2_2/GetCompletionsIT.scala | 245 +++++ .../v2_2/InteractiveSubmissionServiceIT.scala | 27 +- .../testtool/suites/v2_2/InterfaceIT.scala | 17 +- .../v2_2/InterfaceSubscriptionsIT.scala | 125 +-- .../suites/v2_2/ParticipantPruningIT.scala | 17 +- .../v2_2/PartyManagementServiceIT.scala | 5 +- .../testtool/suites/v2_2/SemanticTests.scala | 31 +- .../v2_2/TransactionServiceArgumentsIT.scala | 5 +- .../TransactionServiceAuthorizationIT.scala | 5 +- .../TransactionServiceCorrectnessIT.scala | 5 +- .../v2_2/TransactionServiceExerciseIT.scala | 9 +- .../v2_2/TransactionServiceFiltersIT.scala | 13 +- .../v2_2/TransactionServiceOutputsIT.scala | 5 +- .../TransactionServiceStakeholdersIT.scala | 5 +- .../v2_2/TransactionServiceValidationIT.scala | 5 +- .../v2_2/TransactionServiceVisibilityIT.scala | 5 +- .../suites/v2_2/UpdateServiceQueryIT.scala | 5 +- .../suites/v2_2/UpdateServiceStreamsIT.scala | 9 +- .../testtool/suites/v2_2/UpgradingIT.scala | 111 ++- .../api/testtool/suites/v2_2/VettingIT.scala | 146 +-- .../v2_2/WronglyTypedContractIdIT.scala | 5 +- .../ContractKeysCommandDeduplicationIT.scala | 6 +- .../v2_3/ContractKeysCompanionImplicits.scala | 71 -- .../v2_3/ContractKeysContractIdIT.scala | 16 +- .../ContractKeysExplicitDisclosureIT.scala | 9 +- .../testtool/suites/v2_3/ContractKeysIT.scala | 11 +- ...ContractKeysWronglyTypedContractIdIT.scala | 5 +- .../suites/v2_3/PrefetchContractKeysIT.scala | 9 +- .../suites/v2_dev/EventsDescendantsIT.scala | 15 +- .../ErrorCodeDocumentationGenerator.scala | 8 +- .../ledger/api/auth/RequiredClaims.scala | 12 +- ...ommandCompletionServiceAuthorization.scala | 23 +- .../ContractServiceAuthorization.scala | 5 +- .../TrafficServiceAuthorization.scala | 42 + .../UserManagementServiceAuthorization.scala | 26 +- .../CompletionServiceRequestValidator.scala | 26 +- .../error/groups/CommandExecutionErrors.scala | 16 + .../ledger/metrics/IndexerMetrics.scala | 2 + .../ledger/metrics/ServicesMetrics.scala | 5 - .../participant/state/Reassignment.scala | 5 +- .../state/index/ContractStore.scala | 6 +- .../state/index/IndexCompletionsService.scala | 2 +- .../platform/apiserver/ApiServiceOwner.scala | 3 + .../platform/apiserver/ApiServices.scala | 14 +- .../apiserver/TimedIndexService.scala | 18 +- .../client/RichTrafficServiceClient.scala | 119 +++ .../StoreBackedCommandInterpreter.scala | 22 +- .../ApiCommandCompletionService.scala | 130 ++- .../command/TrafficEnforcementBackend.scala | 127 +++ .../InteractiveSubmissionServiceImpl.scala | 19 +- .../apiserver/services/logging/package.scala | 5 +- .../services/traffic/ApiTrafficService.scala | 48 + .../platform/config/StateServiceConfig.scala | 2 +- .../config/TrafficEnforcementConfig.scala | 73 ++ .../platform/index/InMemoryStateUpdater.scala | 118 +-- .../platform/index/IndexServiceImpl.scala | 12 +- .../canton/platform/indexer/JdbcIndexer.scala | 41 +- .../canton/platform/indexer/package.scala | 28 +- .../parallel/AchsMaintenancePipe.scala | 4 +- .../InitializeParallelIngestion.scala | 211 ++-- .../parallel/ParallelIndexerFactory.scala | 36 +- .../ParallelIndexerSubscription.scala | 50 +- .../platform/store/FlywayMigrations.scala | 6 + .../canton/platform/store/backend/DbDto.scala | 11 + .../store/backend/StorageBackend.scala | 66 +- .../store/backend/UpdateToDbDto.scala | 4 + .../CompletionStorageBackendTemplate.scala | 86 +- .../ContractStorageBackendTemplate.scala | 17 +- .../common/EventStorageBackendTemplate.scala | 12 +- .../common/ParameterStorageBackendImpl.scala | 16 +- .../PostgresContractStorageBackend.scala | 43 +- .../store/cache/ContractKeyStateCache.scala | 19 +- .../store/cache/ContractStateCaches.scala | 160 +++- .../MutableCacheBackedContractStore.scala | 173 ++-- .../platform/store/cache/StateCache.scala | 52 +- .../BufferedCommandCompletionsReader.scala | 15 +- .../store/dao/CommandCompletionsReader.scala | 2 +- .../canton/platform/store/dao/LedgerDao.scala | 2 +- .../store/dao/events/ContractLoader.scala | 71 +- .../store/dao/events/ContractStateEvent.scala | 10 +- .../store/dao/events/ContractsReader.scala | 53 +- .../TransactionLogUpdatesConversions.scala | 12 +- .../store/dao/events/UpdateReader.scala | 31 +- .../interfaces/LedgerDaoContractsReader.scala | 56 +- .../interfaces/TransactionLogUpdate.scala | 13 +- .../ledger/api/auth/RequiredClaimsSpec.scala | 20 + .../ApiServicesRequiredClaimSpec.scala | 31 +- ...ompletionServiceRequestValidatorTest.scala | 78 +- .../StoreBackedCommandInterpreterSpec.scala | 4 +- .../component/AchsIndexComponentTest.scala | 133 +-- .../component/IndexComponentLoadTest.scala | 1 + .../component/IndexComponentTest.scala | 39 +- ...nUniqueContractKeyIndexComponentTest.scala | 38 +- ...oreBasedMaximumLedgerTimeServiceSpec.scala | 8 +- .../index/InMemoryStateUpdaterSpec.scala | 161 ++-- .../ParallelIndexerSubscriptionSpec.scala | 202 +++- .../MultiSynchronizerIndexComponentTest.scala | 2 + .../canton/platform/store/OffsetGen.scala | 14 + .../platform/store/backend/DbDtoSpec.scala | 8 + .../backend/StorageBackendTestValues.scala | 20 +- .../StorageBackendTestsCompletions.scala | 60 +- .../StorageBackendTestsContracts.scala | 289 ++++-- .../backend/StorageBackendTestsEvents.scala | 61 +- .../StorageBackendTestsParameters.scala | 22 +- .../store/backend/UpdateToDbDtoSpec.scala | 41 +- .../store/cache/ContractStateCachesSpec.scala | 797 +++++++++++++++- .../cache/InMemoryFanoutBufferSpec.scala | 6 +- ...bleCacheBackedContractStoreRaceTests.scala | 97 +- .../MutableCacheBackedContractStoreSpec.scala | 724 ++++++++++++-- .../platform/store/cache/StateCacheSpec.scala | 113 +++ ...BufferedCommandCompletionsReaderSpec.scala | 171 ++++ .../store/dao/BufferedStreamsReaderSpec.scala | 6 +- .../dao/JdbcLedgerDaoCompletionsSpec.scala | 28 +- .../dao/JdbcLedgerDaoContractsSpec.scala | 44 +- .../store/dao/JdbcLedgerDaoSuite.scala | 2 +- .../store/dao/SequentialWriteDaoSpec.scala | 15 +- .../main/resources/ledger-api/proto-data.yml | 30 +- .../http/json/v2/JsCommandService.scala | 55 ++ .../canton/http/json/v2/JsUpdateService.scala | 2 +- .../resources/json-api-docs/asyncapi.yaml | 53 +- .../test/resources/json-api-docs/openapi.yaml | 94 +- .../canton/openapi/OpenapiTypesTest.scala | 6 + .../canton/AcsCommitmentBenchmark.scala | 11 +- .../kms/mock/v1/MockKmsDriverFactory.scala | 26 + .../canton/participant/ParticipantNode.scala | 41 +- .../ParticipantNodeParameters.scala | 4 +- .../admin/data/LateLsuRequest.scala | 4 +- .../admin/data/ManualLsuRequest.scala | 4 +- .../GrpcParticipantInspectionService.scala | 8 +- .../GrpcSynchronizerConnectivityService.scala | 41 +- .../inspection/SyncStateInspection.scala | 17 +- .../PartyReplicationTopologyWorkflow.scala | 3 + .../admin/repair/ChangeAssignation.scala | 1 + .../admin/repair/RepairService.scala | 5 +- .../RepairServiceContractsImporter.scala | 4 +- .../config/ParticipantInitConfig.scala | 3 - .../config/ParticipantNodeConfig.scala | 65 +- .../participant/event/AcsChangeListener.scala | 11 +- ...csCommitmentPublicationPostProcessor.scala | 10 +- .../ledger/api/LedgerApiIndexer.scala | 49 +- .../ledger/api/LedgerApiServer.scala | 5 + .../ledger/api/LedgerApiStore.scala | 43 - .../metrics/ParticipantMetrics.scala | 21 +- .../protocol/AbstractMessageProcessor.scala | 2 + .../protocol/ProtocolProcessor.scala | 24 +- .../protocol/TransactionProcessor.scala | 19 + .../reassignment/ReassignmentValidation.scala | 2 +- .../UnassignmentValidationResult.scala | 1 + .../pruning/AcsCommitmentProcessor.scala | 110 ++- .../replica/ParticipantReplicaManager.scala | 16 + ...ticipantPurgeStoresAfterLsuScheduler.scala | 49 +- .../PostLsuPurgeableStoresComputation.scala | 70 ++ .../store/AcsCommitmentStore.scala | 12 +- .../store/SubmissionTrackerStore.scala | 3 + .../SynchronizerConnectivityStatusStore.scala | 2 +- .../store/db/DbAcsCommitmentStore.scala | 70 +- .../store/db/DbSubmissionTrackerStore.scala | 3 + .../DbSynchronizerConnectionConfigStore.scala | 20 +- ...bSynchronizerConnectivityStatusStore.scala | 2 +- .../memory/InMemoryAcsCommitmentStore.scala | 7 +- .../InMemorySubmissionTrackerStore.scala | 2 + .../memory/InMemorySyncPersistentState.scala | 2 +- ...orySynchronizerConnectionConfigStore.scala | 6 +- ...ySynchronizerConnectivityStatusStore.scala | 2 +- .../participant/sync/CantonSyncService.scala | 10 +- .../sync/ConnectedSynchronizer.scala | 8 + .../sync/LogicalSynchronizerUpgrade.scala | 4 +- .../participant/sync/SyncServiceError.scala | 6 +- .../sync/SynchronizerConnectionsManager.scala | 194 ++-- .../sync/SynchronizerMigration.scala | 4 +- .../SynchronizerConnectionConfig.scala | 16 +- .../synchronizer/SynchronizerRegistry.scala | 7 +- .../SynchronizerRegistryHelpers.scala | 13 +- .../grpc/GrpcSynchronizerRegistry.scala | 78 +- .../participant/topology/PackageOps.scala | 5 +- .../ParticipantTopologyDispatcher.scala | 1 + .../ParticipantTopologyValidation.scala | 10 +- .../participant/topology/PartyOps.scala | 7 +- ...SequencerConnectionSuccessorListener.scala | 6 +- .../topology/TopologyComponentFactory.scala | 13 +- .../participant/topology/TopologyLookup.scala | 42 +- .../traffic/TrafficCostEstimator.scala | 2 +- .../canton/participant/util/DAMLe.scala | 16 +- .../participant/admin/PackageOpsTest.scala | 3 + ...artyOnboardingClearanceSchedulerTest.scala | 1 - ...PartyReplicationTopologyWorkflowTest.scala | 29 +- .../protocol/ProtocolProcessorTest.scala | 3 + .../protocol/SubmissionTrackerTest.scala | 8 +- .../protocol/ViewMessageDecrypterTest.scala | 2 + ...pantPurgeStoresAfterLsuSchedulerTest.scala | 59 +- ...ynchronizerConnectionConfigStoreTest.scala | 29 +- ...chronizerConnectivityStatusStoreTest.scala | 8 +- .../store/db/DbContractStoreTest.scala | 2 +- .../SubmissionTrackerStoreTestInMemory.scala | 7 +- .../QueueBasedSynchronizerOutboxTest.scala | 1 + .../StoreBasedSynchronizerOutboxTest.scala | 1 + .../topology/TopologyLookupTest.scala | 104 ++ ...sequencer_bft_administration_service.proto | 4 + .../v30/bft_ordering_service.proto | 1 + .../block/BlockSequencerStateManager.scala | 264 +++-- .../block/update/BlockChunkProcessor.scala | 171 ++-- .../block/update/BlockUpdate.scala | 6 +- .../block/update/BlockUpdateGenerator.scala | 133 ++- .../update/InFlightAggregationHandler.scala | 2 +- .../SequencedSubmissionsValidator.scala | 6 +- .../update/SubmissionRequestValidator.scala | 153 ++- ...firmationRequestAndResponseProcessor.scala | 53 +- .../synchronizer/mediator/Mediator.scala | 14 +- .../mediator/MediatorConfig.scala | 3 - .../synchronizer/mediator/MediatorNode.scala | 89 +- .../mediator/MediatorRuntimeFactory.scala | 4 +- .../mediator/ProcessingQueue.scala | 77 -- .../synchronizer/mediator/VerdictSender.scala | 139 ++- .../GrpcMediatorInspectionService.scala | 22 +- .../metrics/BftOrderingMetrics.scala | 60 +- .../metrics/SynchronizerMetrics.scala | 31 +- .../sequencer/BaseSequencer.scala | 41 +- .../sequencer/DatabaseSequencer.scala | 1 + .../DirectSequencerConnectionPool.scala | 3 + .../LocalSequencerStateEventSignaller.scala | 19 +- .../synchronizer/sequencer/Sequencer.scala | 25 +- .../sequencer/SequencerNode.scala | 20 +- .../sequencer/block/BlockSequencer.scala | 211 ++-- .../block/BlockSequencerFactory.scala | 21 +- .../block/BlockSequencerThroughputCap.scala | 39 +- .../block/DriverBlockSequencerFactory.scala | 17 +- .../BftOrderingSequencerAdminService.scala | 2 + .../admin/SequencerBftAdminData.scala | 20 +- .../canton/sequencing/BftBlockOrderer.scala | 43 +- .../sequencing/BftSequencerFactory.scala | 17 +- .../p2p/grpc/P2PGrpcConnectionManager.scala | 115 ++- .../p2p/grpc/P2PGrpcConnectionState.scala | 163 +++- .../p2p/grpc/P2PGrpcStreamingReceiver.scala | 4 + .../core/BftBlockOrdererConfig.scala | 37 + .../BftOrderingModuleSystemInitializer.scala | 30 +- .../availability/AvailabilityModule.scala | 97 +- .../BatchDisseminationNodeQuotaTracker.scala | 6 +- .../OutputFetchProtocolState.scala | 7 +- .../availability/data/AvailabilityStore.scala | 10 +- .../data/db/DbAvailabilityStore.scala | 130 ++- .../memory/InMemoryAvailabilityStore.scala | 16 +- .../consensus/iss/IssConsensusModule.scala | 59 +- .../consensus/iss/IssSegmentModule.scala | 23 +- .../consensus/iss/TimeoutManager.scala | 25 +- .../consensus/iss/data/EpochStoreReader.scala | 17 +- .../consensus/iss/data/db/DbEpochStore.scala | 47 +- .../iss/data/memory/InMemoryEpochStore.scala | 25 +- .../statetransfer/StateTransferBehavior.scala | 65 +- .../statetransfer/StateTransferManager.scala | 26 +- .../StateTransferMessageSender.scala | 3 +- .../core/modules/output/OutputModule.scala | 446 +++++++-- .../modules/output/OutputModuleMetrics.scala | 8 +- .../output/PekkoBlockSubscription.scala | 179 +++- .../data/db/DbOutputMetadataStore.scala | 28 +- .../memory/InMemoryOutputMetadataStore.scala | 3 + .../modules/p2p/P2PNetworkOutModule.scala | 42 +- .../modules/pruning/PartitionManager.scala | 475 +++++++++ .../core/modules/pruning/PruningModule.scala | 88 +- .../framework/BlockSubscription.scala | 10 +- .../block/bftordering/framework/Module.scala | 43 +- .../framework/data/availability/BatchId.scala | 2 + .../data/ordering/OrderedBlockForOutput.scala | 21 +- .../data/ordering/OrderingMode.scala | 20 + .../framework/data/topology/Membership.scala | 1 + .../framework/modules/Availability.scala | 23 +- .../framework/modules/Consensus.scala | 2 + .../framework/modules/Output.scala | 9 + .../framework/modules/P2PNetworkOut.scala | 2 +- .../framework/modules/Pruning.scala | 6 +- .../bftordering/utils/FairBoundedQueue.scala | 25 +- .../config/SequencerNodeParameterConfig.scala | 9 + .../config/SequencerNodeParameters.scala | 9 + .../sequencer/time/LsuSequencingBounds.scala | 40 +- .../service/DirectSequencerSubscription.scala | 2 +- .../GrpcSequencerAdministrationService.scala | 10 +- .../service/GrpcSequencerService.scala | 23 +- .../SequencerRateLimitManagerImpl.scala | 35 +- ...BftOrderingExplorativeSimulationTest.scala | 21 +- .../BlockSequencerStateAsyncWriterTest.scala | 2 + .../update/BlockChunkProcessorTest.scala | 16 +- .../update/BlockUpdateGeneratorImplTest.scala | 72 +- ...ationRequestAndResponseProcessorTest.scala | 8 +- .../mediator/DefaultVerdictSenderTest.scala | 94 +- .../MediatorEventDeduplicatorTest.scala | 2 +- .../mediator/ProcessingQueueTest.scala | 181 ---- ...ShardedSequentialProcessingQueueTest.scala | 317 ++++++ .../mediator/TestVerdictSender.scala | 16 +- .../sequencer/BaseSequencerTest.scala | 1 + .../sequencer/SequencerApiTest.scala | 67 +- .../sequencer/block/BlockSequencerTest.scala | 33 +- ...BftOrderingSequencerAdminServiceTest.scala | 8 +- .../PekkoBlockSubscriptionTest.scala | 77 +- .../core/modules/PruningModuleTest.scala | 30 +- .../AvailabilityModuleDisseminationTest.scala | 53 +- .../AvailabilityModuleOutputFetchTest.scala | 82 +- .../AvailabilityModuleTestUtils.scala | 11 +- ...tchDisseminationNodeQuotaTrackerTest.scala | 4 +- .../data/AvailabilityStoreTest.scala | 77 +- .../availability/data/model/Command.scala | 5 +- .../availability/data/model/Generator.scala | 22 +- .../iss/IssConsensusModuleTest.scala | 291 ++++-- .../iss/StateTransferBehaviorTest.scala | 4 +- .../consensus/iss/TimeoutManagerTest.scala | 1 + .../consensus/iss/data/EpochStoreTest.scala | 62 +- .../StateTransferManagerTest.scala | 7 +- .../PbftMessageValidatorImplTest.scala | 8 +- ...ksRecoveredFromConsensusMessagesTest.scala | 204 ++++ .../modules/output/OutputModuleTest.scala | 368 ++++--- .../SimulationOutputMetadataStore.scala | 6 +- .../modules/p2p/P2PNetworkOutModuleTest.scala | 8 +- .../pruning/PartitionManagerTest.scala | 279 ++++++ .../pruning/PartitionMigrationTest.scala | 123 +++ .../framework/EmptyBlockSubscription.scala | 17 +- .../SimulationBlockSubscription.scala | 9 +- .../simulation/FutureSimulator.scala | 5 +- .../framework/simulation/Simulation.scala | 3 +- .../future/FutureSimulatorAllocator.scala | 7 +- .../BftOrderingSimulationTest.scala | 8 +- .../SequencerSnapshotOnboardingManager.scala | 16 +- ...imeAdvancingTopologySubscriberV1Test.scala | 5 + ...imeAdvancingTopologySubscriberV2Test.scala | 11 + .../sequencer/SequencerStateManagerTest.scala | 33 +- .../traffic/RateLimitManagerTesting.scala | 3 + .../SequencerRateLimitManagerImplTest.scala | 2 +- .../canton/MockedNodeParameters.scala | 2 +- .../provider/symbolic/SymbolicCrypto.scala | 2 + .../symbolic/SymbolicPrivateCrypto.scala | 6 + .../symbolic/SymbolicPureCrypto.scala | 12 +- .../canton/metrics/CommonMockMetrics.scala | 17 + .../topology/TestingIdentityFactory.scala | 5 +- .../traffic-enforcement/api/protobuf/buf.yaml | 5 + .../canton/tea/scalapb/package.proto | 14 + .../canton/tea/v1/traffic_service.proto | 62 ++ .../canton/tea/TrafficEnforcementApp.scala | 86 ++ .../tea/TrafficEnforcementService.scala | 122 +++ .../tea/TrafficEnforcementServiceGrpc.scala | 87 ++ .../tea/projection/CloseableProjection.scala | 65 ++ .../tea/projection/ProjectionEvent.scala | 180 ++++ .../canton/tea/projection/TeaProjection.scala | 133 +++ .../tea/projection/TeaTrafficStore.scala | 69 ++ .../tea/projection/db/TeaDbProjection.scala | 103 ++ .../tea/projection/db/TeaDbTrafficStore.scala | 205 ++++ .../memory/TeaMemoryProjection.scala | 51 + .../memory/TeaMemoryTrafficStore.scala | 123 +++ .../tea/projection/TeaProjectionTest.scala | 257 +++++ .../tea/projection/TeaTrafficStoreTest.scala | 221 +++++ .../projection/db/DbTeaProjectionTest.scala | 81 ++ .../projection/db/DbTeaTrafficStoreTest.scala | 52 + .../memory/MemoryTeaProjectionTest.scala | 33 + .../memory/MemoryTeaTrafficStoreTest.scala | 16 + .../src/main/daml/examples/Conformance.daml | 7 + .../transcode/daml_lf/LfSchemaProcessor.scala | 13 + .../daml_lf/SchemaProcessorSpecDefault.scala | 27 +- .../transcode/daml_lf/package.scala | 8 - .../transcode/schema/CodecVisitor.scala | 7 + .../digitalasset/transcode/schema/Debug.scala | 11 +- .../transcode/schema/Descriptor.scala | 6 + .../schema/DescriptorSchemaProcessor.scala | 1 + .../transcode/schema/DescriptorVisitor.scala | 1 + .../transcode/schema/Pickler.scala | 1 + .../transcode/schema/SchemaVisitor.scala | 15 + .../conformance/data/Primitives.scala | 4 + .../conformance/generator/DamlRoundtrip.scala | 9 + .../transcode/conformance/package.scala | 5 + .../propertygenerators/SchemaGenerator.scala | 1 + ...AwarePackageSelectionIntegrationTest.scala | 2 +- canton/release-notes/3.5.1.md | 899 ++++++++++++++++++ canton/release-notes/3.5.2.md | 69 ++ canton/release-notes/3.5.3.md | 30 + canton/release-notes/3.5.4.md | 51 + canton/release-notes/3.5.5.md | 68 ++ canton/release-notes/3.5.6.md | 77 ++ canton/shared_dependencies.json | 4 +- 681 files changed, 22183 insertions(+), 6378 deletions(-) create mode 100644 canton/base/testing-utils/src/main/resources/logback-test.xml create mode 100644 canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto create mode 100644 canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala create mode 100644 canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala create mode 100644 canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala create mode 100644 canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala create mode 100644 canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala create mode 100644 canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala create mode 100644 canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala create mode 100644 canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala create mode 100644 canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala create mode 100644 canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala create mode 100644 canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala create mode 100644 canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala create mode 100644 canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala create mode 100644 canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 create mode 100644 canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql create mode 100644 canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 create mode 100644 canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql create mode 100644 canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 create mode 100644 canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql create mode 100644 canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 create mode 100644 canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql create mode 100644 canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 create mode 100644 canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql create mode 100644 canton/community/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/UnsupportedContractIdEngineSpec.scala create mode 100644 canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/BaseEnvironmentDefinition.scala create mode 100644 canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasCycleUtils.scala create mode 100644 canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/HasTrailingNoneUtils.scala create mode 100644 canton/community/integration-testing/src/main/scala/com/digitalasset/canton/integration/plugins/UseLedgerApiTestTool.scala create mode 100644 canton/community/ledger-test-tool/src/main/resources/logback.xml delete mode 100644 canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/CompanionImplicits.scala create mode 100644 canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_2/GetCompletionsIT.scala delete mode 100644 canton/community/ledger-test-tool/src/main/scala/com/daml/ledger/api/testtool/suites/v2_3/ContractKeysCompanionImplicits.scala create mode 100644 canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/ledger/api/auth/services/TrafficServiceAuthorization.scala create mode 100644 canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/client/RichTrafficServiceClient.scala create mode 100644 canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/command/TrafficEnforcementBackend.scala create mode 100644 canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/apiserver/services/traffic/ApiTrafficService.scala create mode 100644 canton/community/ledger/ledger-api-core/src/main/scala/com/digitalasset/canton/platform/config/TrafficEnforcementConfig.scala create mode 100644 canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/OffsetGen.scala create mode 100644 canton/community/ledger/ledger-api-core/src/test/scala/com/digitalasset/canton/platform/store/dao/BufferedCommandCompletionsReaderSpec.scala create mode 100644 canton/community/participant/src/main/scala/com/digitalasset/canton/participant/scheduler/PostLsuPurgeableStoresComputation.scala create mode 100644 canton/community/participant/src/test/scala/com/digitalasset/canton/participant/topology/TopologyLookupTest.scala delete mode 100644 canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueue.scala create mode 100644 canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManager.scala create mode 100644 canton/community/synchronizer/src/main/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/framework/data/ordering/OrderingMode.scala delete mode 100644 canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ProcessingQueueTest.scala create mode 100644 canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/mediator/ShardedSequentialProcessingQueueTest.scala create mode 100644 canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/output/BlocksRecoveredFromConsensusMessagesTest.scala create mode 100644 canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionManagerTest.scala create mode 100644 canton/community/synchronizer/src/test/scala/com/digitalasset/canton/synchronizer/sequencer/block/bftordering/core/modules/pruning/PartitionMigrationTest.scala create mode 100644 canton/community/traffic-enforcement/api/protobuf/buf.yaml create mode 100644 canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/scalapb/package.proto create mode 100644 canton/community/traffic-enforcement/api/protobuf/com/digitalasset/canton/tea/v1/traffic_service.proto create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementApp.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementService.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/TrafficEnforcementServiceGrpc.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/CloseableProjection.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/ProjectionEvent.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaProjection.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/TeaTrafficStore.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbTrafficStore.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryProjection.scala create mode 100644 canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/memory/TeaMemoryTrafficStore.scala create mode 100644 canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaProjectionTest.scala create mode 100644 canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/TeaTrafficStoreTest.scala create mode 100644 canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaProjectionTest.scala create mode 100644 canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/db/DbTeaTrafficStoreTest.scala create mode 100644 canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaProjectionTest.scala create mode 100644 canton/community/traffic-enforcement/component/src/test/scala/com/digitalasset/canton/tea/projection/memory/MemoryTeaTrafficStoreTest.scala delete mode 100644 canton/community/transcode/daml-lf/src/test/scala/com/digitalasset/transcode/daml_lf/package.scala create mode 100644 canton/release-notes/3.5.1.md create mode 100644 canton/release-notes/3.5.2.md create mode 100644 canton/release-notes/3.5.3.md create mode 100644 canton/release-notes/3.5.4.md create mode 100644 canton/release-notes/3.5.5.md create mode 100644 canton/release-notes/3.5.6.md diff --git a/canton/.proto_snapshot_image.bin.gz b/canton/.proto_snapshot_image.bin.gz index 36562efde4dbe146bfcda28ccd945716066d9a01..e53644f1d6855bdba0070734f732e74e87906db3 100644 GIT binary patch literal 517511 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 f}!Xa-1)fuVcE@@X~Dmk6YQd@mm4en*g>c( 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

XnQlCi;*t^xyd6#yuA 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%0AZ0pTyMK)%NTF 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

AW=XD&jg9BWCMNO| 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

fkE;-k|9M~tIXD~!WbFx+ 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%0DU*oYm^aboT#df7Yyn@jfq> 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 z�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 'label' - from __contracts - where contract_key = jsonb_build_object(...) - order by created_at_ix - ``` - -### Daml-LF 2.3 - -A new version of Daml-LF is released: Daml-LF 2.3. Its main features are: - -- [`DA.Crypto.Text`](https://docs.digitalasset.com/build/3.5/reference/daml/stdlib/DA-Crypto-Text.html), - originally released in 3.4 in early access (alpha) status, is part of LF 2.3, - which means it is now marked as stable. -- Support for Contract Keys. - -#### Targeting LF 2.3 - -If you want to use new features available in the LF 2.3, select it explicitly as compilation target by setting the -`--target=2.3`, either as direct argument on the command line or as part of a `daml.yaml`: - -``` -sdk-version: 3.5.1 -name: some-name -source: daml -version: 0.0.1 -dependencies: - - daml-prim - - daml-stdlib -build-options: -- --target=2.3 -``` - -After changing the settings, the source code must be recompiled. Please note that this will cause the package id to -change, which should be accompanied by a version change. - -### Logical Synchronizer Upgrades - - -### DA BFT Beta - -DA BFT is a new ordering service as part of the synchronizer that will replace the current single-leader CometBFT ordering service on the Global Synchronizer with a parallel, multi-leader consensus architecture, enabling significantly higher transaction throughput and fault tolerance. - -As part of this release DA BFT is ready in beta form for early access testing, but not recommended for production or close-to-production testing yet. - -### Multi Synchronizer Alpha - -Multi-synchronizer support is available in early access and has to be enabled explicitly. -This feature should only be used in test environments. - -To enable contract reassignment across synchronizers, the flag `PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER` must be activated on all participants hosting a stakeholder of the contract on both the source and target synchronizers. For a synchronizer, it can be done as follows: - -``` -participant.topology.synchronizer_trust_certificates.propose( - p.id, - synchronizerId, - featureFlags = Seq(ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer), -) -``` - - -## Functional Changes - -### Party Replication - -#### Offline party replication - -Concluding an offline party replication by clearing the onboarding flag now includes two major updates -when using protocol version 35: -- Added crash resilience for ongoing clearances. -- Automatic scheduling for clearances when a participant (re)connects to the synchronizer. - -These changes apply only to the `participant.parties.import_party_acs` and -`participant.parties.clear_party_onboarding_flag` endpoints. - -Note: The replicated party ID must be included in the party ACS import call to enable automatic -scheduling. The original behaviour is retained for protocol version 34. - -#### Party replication onboarding topology event is exposed on Ledger API - -The `PartyToParticipant` topology "onboarding" state used in the process of replicating a party with existing -contracts is now visible via the Ledger API when a party onboards on a synchronizer on protocol version 35 or higher. -Starting with PV=35, the newly introduced `ParticipantAuthorizationOnboarding` Ledger API topology event signals -the beginning of party replication and transitions to `ParticipantAuthorizationAdded` once the party's ACS is fully -visible on the Ledger API. - -#### Preview: Online party replication - -- Added the file-based online party replication command `participant.parties.add_party_with_acs_async` to - be used along with `participant.parties.export_party_acs` and instead of the sequencer-channel-based - `add_party_async` command. -- The online party replication status command now returns status in a very different, "vector-status" format - rather than the old "oneof" style. This impacts the `participant.parties.get_add_party_status` command and - `com.digitalasset.canton.admin.participant.v30.PartyManagementService.GetAddPartyStatus` gRPC response type. -- The participant configuration to enable online party replication has been renamed to - `alpha-online-party-replication-support` from `unsafe-online-party-replication` for consistency with other - alpha features and to reflect that the default file-based mode is more secure not relying on sequencer - channels. -- The sequencer configuration to enable sequencer channels for online party replication has been renamed to - `unsafe-sequencer-channel-support` from `unsafe-enable-online-party-replication` for consistency and to - refer specifically to sequencer channels. - -#### Minor Improvements - -- Onboarding party submission prevention: Ensures a participant does not submit a transaction or reassignment on behalf - of an onboarding party. -- Upgraded gRPC to 1.81.0 and AWS SDK to 2.44.3 to resolve Netty 4.1.130 CVEs (CVE-2026-33870, CVE-2026-33871). - -### New Transaction Hashing Scheme v3 - -- A new hashing scheme version `HASHING_SCHEME_VERSION_V3` has been introduced that includes the transaction's `max_record_time` in the hash computation and covers the new transaction node and fields of contract keys. This new version is avaialble from Protocol Version 35. -- See the [hashing algorithm documentation](https://docs.digitalasset-staging.com/build/3.5/explanations/external-signing/external_signing_hashing_algorithm.html#summary-of-differences-between-v2-and-v3) for the updated version. -- The `max_record_time` is now enforced by all confirming participants. -- The Ledger API and Ledger JSON API prepare `InteractiveSubmissionService` has been modified to take in a specific hashing scheme version in the request. -The default hashing scheme is `HASHING_SCHEME_VERSION_V2`. Integrators are encouraged to move to `HASHING_SCHEME_VERSION_V3` for synchronizers using protocol version 35. -In particular, usage of **contract keys** requires `HASHING_SCHEME_VERSION_V3`. See the versioning [documentation](https://docs.digitalasset-staging.com/build/3.5/explanations/external-signing/external_signing_hashing_algorithm.html#hashing-scheme-version) for details. - -### Active Contracts Head Snapshot (ACHS) - -The Active Contracts Head Snapshot (ACHS) is a new optional feature that maintains a continuously updated snapshot of -the currently active contracts. When enabled, the ACHS accelerates `GetActiveContracts` (ACS) queries by allowing them -to read directly from a pre-computed snapshot rather than scanning the full event log to reconstruct the active set. - -ACHS is disabled by default. To enable it, configure the `achs-config` block under the participant's indexer settings: -``` -canton.participants..parameters.ledger-api-server.indexer.achs-config { - valid-at-distance-target = 1000000 - last-populated-distance-target = 500000 -} -``` - -The `valid-at-distance-target` controls how far behind the ledger end (in event sequential IDs) the snapshot's validity -point is maintained. The ACHS is not used for serving queries below its validity point, logging at INFO level "ACHS for -skipped since validAt (...) already surpassed requested activeAt (...)". If the `valid-at-distance-target` -value is too small, long-running ACS queries may observe the ACHS validity point -moving (mid-stream) past their requested offset, causing the stream to fall back to the slower filter tables query, logging -at INFO level "ACHS stream for fell back to filter tables from (...) since validAt (...) surpassed activeAtEventSeqId (...)". If -the value is too large, the tail portion of the ACS (between the ACHS validity point and the requested offset) must be -resolved from the filter tables, making that last segment more expensive. - -As described above, when the ACHS validity point moves or is past the requested offset, an info-level log message is -emitted indicating that the stream fell back to the filter tables. -Two corresponding metrics, `achs_skips` and `achs_midstream_fallbacks`, are available under `daml.participant.api.index` -to help operators monitor the frequency of these fallbacks and tune the `valid-at-distance-target` accordingly. - -The `last-populated-distance-target` controls the additional lag (in event -sequential IDs) for the population of ACHS in order to store only the long-lived contracts. A larger value reduces -database I/O by skipping short-lived contracts that are created and archived before they would be added to the snapshot. -However, setting it too large increases the cost of the remaining ACS tail, as more data must be fetched from the filter -tables to cover the gap between the last populated point and the ACHS validity point. - -Further tuning parameters include: -- `population-parallelism`: number of parallel threads for adding activations to the ACHS during normal operation. -- `removal-parallelism`: number of parallel threads for removing deactivated activations from the ACHS during normal operation. -- `aggregation-threshold`: minimum batch size (in event sequential IDs) before ACHS maintenance work is emitted. -- `init-parallelism`: number of parallel threads for ACHS population and removal during initialization. -- `init-aggregation-threshold`: minimum batch size (in event sequential IDs) for ACHS maintenance during initialization. -- `buffer-size`: size of the internal buffer between the indexer pipeline and the ACHS maintenance flow. - -The `deactivation_distances` histogram metric which is available under `daml.participant.api.indexer.deactivation_distances` -can help operators understand the distribution of contract lifetimes (the event sequential ID distance between a contract's -activation and its deactivation) and set an appropriate `last-populated-distance-target`. Ideally, the population distance -should be large enough so that most short-lived contracts are already deactivated and thus not added to the snapshot. - -Three gauge metrics are available under `daml.participant.api.indexer` to monitor the ACHS state: -- `achs_valid_at`: the event sequential ID at which the ACHS is currently valid. ACS queries with a requested offset - at or after this value can read directly from the ACHS. -- `achs_last_populated`: the last event sequential ID for which activations were added to the ACHS. -- `achs_last_removed`: the last event sequential ID for which deactivations were looked up and the corresponding - activations were removed from the ACHS. - -### Hardened Error Handling in Sequencer Connect Service - -We have implemented strict error sanitization and rewording for the SequencerConnectService to mitigate information leakage. -Detailed internal error messages are now redacted before being sent to clients. - -If detailed diagnostics are required in a non-production environment, sanitization can be toggled off via: - -``` -canton.monitoring.sanitize-public-error-messages = false -``` - -### Ignoring of offboarded sequencers for submission requests - -In the case where sequencers are offboarded but remain online and kept in the connectivity configuration, it was still possible that members pick them as the target for submission requests. The submission would fail, but the member would incur a delay as it requires retrying. -This has now changed, and offboarded sequencers are ignored when sending submission requests. - -#### API Changes - -The previous method of returning errors via response fields has been removed in favor of canonical gRPC error propagation. -The following fields are now obsolete: - -- `HandshakeResponse.value.failure` -- `VerifyActiveResponse.value.failure` - -Errors are now communicated strictly through `io.grpc.Status` codes to ensure a consistent and secure interface. - -Status codes have changed as follows: - -- SequencerAuthenticationService.challenge newly fails with `INVALID_ARGUMENT` (instead of `FAILED_PRECONDITION`), - if the client does not support the sequencer's protocol version. -- SequencerConnectService newly fails with `INVALID_ARGUMENT` (instead of `FAILED_PRECONDITION`) if a non-participant tries to connect. -- SequencerConnectService.registerOnboardingTopologyTransactions newly fails with `INTERNAL` (instead of `FAILED_PRECONDITIONS`) -- if there are no dynamic synchronizer parameters. -- SequencerConnectService.registerOnboardingTopologyTransactions newly fails with `FAILED_PRECONDITION` if -- the transactions cannot be added to the topology state and sanitization of error messages is enabled. - -### Mediator Crash Fault Tolerance - -The mediator is now crash fault-tolerant and guarantees that all verdicts will eventually be persisted and available on the inspection API. - -### Enhanced Reliability for `GetHighestOffsetByTimestamp` - -Previously, the `GetHighestOffsetByTimestamp` RPC and the `find_highest_offset_by_timestamp` console command could return offsets not yet synced with the participant's local cache. Furthermore, forcing a query with a future timestamp resulted in an error. - -Specific changes: -- The required state is now retrieved atomically via a consistent database snapshot. -- The endpoint now includes an internal barrier (waiting up to 10 seconds) to ensure the local Ledger API cache catches up with the database before returning the offset. -- When `force` is true, requesting a future timestamp now gracefully returns the current ledger end instead of failing. - -No migration required. - -### ACS stream continuation - -The `GetActiveContracts` stream request has been extended with an optional `stream_continuation_token` field that allows -clients to continue an interrupted ACS stream from the last element which made through. The field can be populated with -the `stream_continuation_token` field of the last response element received before the interruption, and the stream will -continue from the next element after that. - -### ACS Ledger API counting - -Introduced a new memory-efficient consoled command `participant.ledger_api.acs.count()` -to count the number of active contracts on a participant node. - -> Note: This command is currently under the Testing feature flag. - -### ACS pagination - -A new, `GetActiveContractsPage` endpoint added to State Service API. This enables the client to retrieve the ACS in -paginated form, by specifying a `max_page_size`. The pages can be accessed sequentially by using the `page_token` -field. The token can be obtained from the `GetActiveContractsPageResponse` of the last page. - -### GetUpdates stream in descending order of events - -The `GetUpdatesRequest` object has new optional parameter `descending_order`. When this parameter is `true` the events -are streamed from the newest to the oldest ones. The pages can be accessed sequentially by using the `page_token` -field. - -### GetUpdates pagination -A new `GetUpdatesPage` endpoint has been added to Update Service API. THis allows retrieval of updates in paginated -form instead of requesting the stream. - -### Improvements for `repair.add` and migration advice - -The `participant.repair.add` admin command has been revised to use the new `ImportAcs` backend, bringing significant -memory performance improvements, stricter default safety validations, and several new parameters. - -#### Important behavioral change: strict `Validation` by default - -Previously, `repair.add` implicitly accepted all injected contracts without re-evaluating their cryptographic hashes. To -prevent accidental data corruption, the command now defaults to **Validation** mode ( -`contractImportMode = ContractImportMode.Validation`). - -- **Impact:** If you have existing scripts or recovery procedures that inject manually modified, synthetic, or - inconsistent contracts (where the payload does not strictly match the `ContractId` hash), they will now fail with a - `"Failed to authenticate contract with id"` error. -- **Migration:** To bypass this cryptographic validation and restore the legacy behavior, explicitly pass the `Accept` - mode in your command call: - ```scala - participant.repair.add( - synchronizerId = mySynchronizer, - protocolVersion = myProtocolVersion, - contracts = myContracts, - contractImportMode = ContractImportMode.Accept // Bypasses strict validation - ) - ``` - -#### New parameters - -The command signature has been expanded to support several optional parameters: - -- `workflowIdPrefix`: Allows you to set a custom prefix for the generated workflow ID to easily track the repair - transactions (defaults to `import-`). -- `contractImportMode`: Choose between `Validation` (default, validates that contract IDs comply with the scheme - associated to the synchronizer where the contracts are assigned), or `Accept` the contracts as they are (if you know - what you are doing). -- `representativePackageIdOverride`: Allows you to remap or override the representative package IDs of the contracts as - they are imported. -- `excludedStakeholders`: When defined, any contract that has one or more of these parties as a stakeholder will not be - added. - -### Improved party and repair ACS imports +### Topic A +Template for a bigger topic +#### Background +#### Specific Changes +#### Impact and Migration -We have completely overhauled the ACS import endpoints for both party replication and participant repair to be -memory-efficient streaming endpoints: +### New `GetCompletions` endpoint on the command completion service +#### Background +The command completion service only offered `CompletionStream`, which always filters by a single user and a non-empty set of parties. There was no way to stream completions across all parties. -- Console command `participant.parties.import_party_acs` -- Console command `participant.repair.import_acs` -- gRPC RPC `PartyManagementService.ImportPartyAcs` -- gRPC RPC `ParticipantRepairService.ImportAcs` +#### Specific Changes +A new completion streaming endpoint is introduced: `GetCompletions` (gRPC) or `/commands/command-completions` (JSON API). This endpoint offers more flexible filtering semantics compared to the existing `CompletionStream`, `/commands/completions` endpoint. It filters by `parties` only; there is no user filtering: +- A non-empty `parties` filters to those parties and requires `ReadAs` (or `ActAs`) for each. +- An empty `parties` returns completions for all parties and requires `ReadAsAnyParty` (or `ActAsAnyParty`). -This resolves previous memory limitations, as these endpoints no longer load the entire ACS snapshot into memory at -once. +In all other respects it behaves like `CompletionStream`, which becomes deprecated. -#### Action required: Breaking API change +#### Impact and Migration +This is an additive change. `CompletionStream` is unchanged, so no migration is required. -The `synchronizerId` is now a **mandatory** first parameter for both the `import_party_acs` and `import_acs` console -commands as well as their analogous gRPC endpoints. You will need to update any existing scripts. +### Minor Improvements +- Added latency metrics for signing and decryption operations. +- Updated nix package to source from GHCR and bump dpm version to 1.0.20 for remote dar support +- Fixed a BFT ordering sequencer crash-recovery issue affecting freshly onboarded nodes. If such a node crashed and restarted while still within its onboarding start epoch, on restart it could attempt to recover the output module from a block below its durable lower bound (a block that was never stored by this node), which could leave the node stuck. The onboarding boundary block is now persisted with its BFT time to seed BFT-time computation across a restart, the node's own activation time is reconstructed when needed for crash recovery, and output-module recovery is clamped to the durable lower bound. +- LSU: improved handshake between a sequencer and its successor: the physical synchronizer id and sequencer id are now validated. -**For `import_party_acs`:** +### Preview Features +- preview feature -- **Old usage:** `participant.parties.import_party_acs("canton-acs-export.gz")` -- **New usage:** `participant.parties.import_party_acs(mySynchronizerId, importFilePath = "canton-acs-export.gz")` +## Bugfixes -**For `import_acs`:** +### (YY-nnn, Risk): Title -- **Old usage:** `participant.repair.import_acs("canton-acs-export.gz")` -- **New usage:** `participant.repair.import_acs(mySynchronizerId, importFilePath = "canton-acs-export.gz")` +#### Issue Description -Because of the mandatory `synchronizerId` parameter, to import a multi-synchronizer ACS snapshot, you must now call the -endpoint sequentially for each synchronizer your participant is connected to, using the exact same snapshot file. The -import process will ignore any contracts in the snapshot that are associated to a different synchronizer. +#### Affected Deployments -##### Details on the gRPC `ImportAcs` repair endpoint +#### Affected Versions -The `ImportAcs` and `ImportAcsV2` RPCs have been consolidated, introducing the following breaking changes and migration -steps: +#### Impact -- **Endpoint removed:** `ImportAcsV2` (along with its request/response messages) is completely removed. All clients must - migrate to the standard `ImportAcs` RPC. -- **Request signature and type changes:** - - Fields `workflow_id_prefix` (2), `contract_import_mode` (3), and `representative_package_id_override` (5) in - `ImportAcsRequest` are now explicitly `optional`. - - A new `optional string synchronizer_id = 6` field was added. - - **Migration (ScalaPB):** Adding `optional` changes generated code from base types to `Option[T]`. Existing clients - will fail to compile and must be updated to wrap assigned values (e.g., `workflowIdPrefix = Some("prefix")`) and - explicitly handle reading `Option` types. -- **Behavioral change (`synchronizer_id`):** When filtering by synchronizer, mismatched contracts are now ignored. This - breaks previous logic that relied on the import strictly aborting upon a mismatch. +#### Symptom -##### Details on the gRPC `ImportPartyAcs` party replication endpoint +#### Workaround -The `ImportPartyAcs` endpoint underwent the exact same consolidation (removing `ImportPartyAcsV2`), streaming semantics -updates, generated code changes (ScalaPB `Option[T]`), and mismatched synchronizer behavior (ignoring rather than -failing) as `ImportAcs`. +#### Likeliness -**Key differences specific to `ImportPartyAcs`:** - -- **New capability (`party_id`):** A new `optional string party_id = 6` field was added. Providing this in the first - request of the stream enables automatic, crash-resilient scheduling of the onboarding flag clearance. If omitted, the - participant logs a warning, and the flag must be cleared manually. - -### Topology-Aware Package Selection (TAPS) improvements - -Topology-Aware Package Selection (TAPS) refinement for handling inconsistent vetting states: -- The algorithm now considers a party's package vetting state only for packages required by that party in the interpreted transaction. - It starts with a minimal set of restrictions derived from the command's root nodes and progressively accumulates more restrictions over a configurable number of passes. - This iterative process increases the likelihood of finding a valid package selection set for the routing of the transaction. -- The maximum number of TAPS passes can be set at the request-level via the optional `taps_max_passes` field in `Commands` or `PrepareSubmissionRequest` messages. - If not specified, the default value is taken from the participant configuration via `participants.participant.ledger-api.topology-aware-package-selection.max-passes-default` (defaults to `3`). - A hard limit is enforced by `participants.participant.ledger-api.topology-aware-package-selection.max-passes-limit` (defaults to `4`). -- TAPS now ignores unvetted dependencies of packages that are not required for interpretation. - complying now with the support of unvetted dependencies in the Canton protocol. - -### Ledger API Improvements - -- ApiRequestLogger now also used by Ledger JSON Api. Changes: - - Redundant Request TID removed from logs. - - Additional CLI options added: `--log-access` captures API access logs in a separate file (default: `log/canton_access.log`), and `--log-access-errors` captures API access errors in a separate file (default: `log/canton_access_error.log`). - - Additional config options added: `debugInProcessRequests` logs in-process gRPC requests at DEBUG instead of TRACE, and `prefixGrpcAddresses` prefixes gRPC client addresses with `grpc:` (enabled by default). -- LedgerAPI ListKnownParties supports an optional prefix filter argument filterParty. - The respective JSON API endpoint now additionally supports `identity-provider-id` as - an optional argument, as well as `filter-party`. -- Protect the admin participant from self lock-out. It is now impossible for an admin to remove own admin rights or - delete itself. -- On Ledger API interface subscriptions, the `CreatedEvent.interface_views` now returns the ID of the package containing - the interface implementation that was used to compute the specific interface view as `InterfaceView.implementation_package_id`. -- OffsetCheckpoints are now always generated when an open-ended updates or completions stream is requested, even if there - are no updates. The checkpoint can have the same offset as the exclusive start of the stream, making checkpoints visible - even when starting from the ledger end. This enables client systems to recognize when the ledger end is advancing, - even if the stream of updates is inactive. -- Extended the set of characters allowed in user-id in the ledger api to contain brackets: `()`. - This also makes those characters accepted as part of the `sub` claims in JWT tokens. -- Functionality for managing internal and external parties has been improved, removing previous asymmetry: - - User rights can now be assigned to an external party during allocation. - - External parties can be allocated by the user themselves in the self-administration mode. - Please note that users in self-administration mode can allocate up to N parties, depending on a setting of the parameter - ``` - canton.participants..ledger-api.party-management-service.max-self-allocated-parties - ``` - By default the value of this parameter is 0. -- An IDP administrator can now only allocate parties confined to their own IDP perimeter. - - -## Performance Improvements - -### Session Signing Keys - -Session signing keys can now be used to reduce the number of calls to external KMS (Key Management Service) providers. When enabled, session signing keys are generated and cached locally for a limited duration and used for signing operations during their validity period. - -Please read the documentation on [Session Signing Keys](https://docs.digitalasset.com/operate/3.5/howtos/secure/keys/session_signing_keys.html) for details on how to enable and configure this feature. -Session signing keys are only available from Protocol Version 35 and are not enabled by default. - -### Compatible sibling views compression - -In protocol version 35, each envelope in `TransactionConfirmationRequest` contains multiple views grouped by recipients instead of one envelope per view. -Assignment and re-assignments also use this new format, but they always have one view. - -### Single Topology Transaction for External Parties - -Multiple topology transactions for external parties can now be represented with a single `PartyToParticipant` topology transaction. - -The `generateExternalPartyTopology` endpoint on the Ledger API now returns a single `PartyToParticipant` topology transaction to onboard the party. -The transaction contains signing threshold and signing keys. This effectively deprecate the usage of `PartyToKeyMapping`. -For parties with signing keys both in `PartyToParticipant` and `PartyToKeyMapping`, the keys from `PartyToParticipant` take precedence. - -Deprecated usage of `PartyToKeyMapping`. The functionality provided by `PartyToKeyMapping` is now available directly in `PartyToParticipant`. -Please use `PartyToParticipant` for new transactions. `PartyToKeyMapping` is still fully supported in this version (including existing and new transactions). -In future version, creation of new `PartyToKeyMapping` transactions may be disallowed. - -Deprecated `TopologyManagerReadService.ListAll` in favor of `ListAllV2`, which uses an inclusion -list (`include_mappings`) instead of an exclusion list (`exclude_mappings`). This avoids sending -mapping codes unknown to older servers. The console method `topology.transactions.list` now calls -`ListAllV2` by default and only falls back to `ListAll` when targeting a 3.4 node. The -`excludeMappings` and `protocolVersion` parameters of `topology.transactions.list` are deprecated; -use `filterMappings` instead. - -Deprecated `TopologyManagerReadService.ExportTopologySnapshot` and `TopologyManagerWriteService.ImportTopologySnapshot`, -along with their console counterparts `topology.transactions.export_topology_snapshot`, -`topology.transactions.import_topology_snapshot`, `topology.transactions.import_topology_snapshot_from`, -and `topology.transactions.export_identity_transactions`. -Please use the corresponding `V2` variants (`ExportTopologySnapshotV2` / `ImportTopologySnapshotV2`, -`export_topology_snapshotV2`, `import_topology_snapshotV2`, `import_topology_snapshot_fromV2`, -`export_identity_transactionsV2`) instead, which use an updated internal bytestring format. - -Deprecated `SequencerInitializationService.InitializeSequencerFromGenesisState`, -`SequencerInitializationService.InitializeSequencerFromOnboardingState`, -`SequencerAdministrationService.OnboardingState`, and -`TopologyManagerReadService.GenesisState`, along with their console counterparts -`setup.assign_from_genesis_state`, `setup.assign_from_onboarding_state`, -`setup.onboarding_state_for_sequencer`, `setup.onboarding_state_at_timestamp`, -and `topology.transactions.genesis_state`. -Please use the corresponding `V2` variants (`InitializeSequencerFromGenesisStateV2`, -`InitializeSequencerFromOnboardingStateV2`, `OnboardingStateV2`, `GenesisStateV2`, -`assign_from_genesis_stateV2`, `assign_from_onboarding_stateV2`, -`onboarding_state_for_sequencerV2`, `onboarding_state_at_timestampV2`, -`genesis_stateV2`) instead, which use an updated internal bytestring format -that enables streaming ingestion, making snapshot export and import significantly less memory-intensive. - -### Minor Performance Improvements - -- The Postgres connection tuning configuration of the indexer is now separated from the configuration of the Ledger API server - (`canton.participants..ledger-api.postgres-data-source`). - The new configuration section `canton.participants..parameters.ledger-api-server.indexer.postgres-data-source` should - be used instead to tune the indexer's Postgres connections. -- A new indexer pipeline batching strategy added under the feature flag `useWeighetdBatching`. When switched on, the - batches are created using their estimated database processing time using the `submissionBatchInsertionSize` as a limit - for individual batches -- Changed the `CompressedBatch` structure in the sequencer protocol for protocol version 35 to separately keep recipients and envelopes (from `gzip(Seq((recp1, payload1), (recp2, payload2)))` to `gzip(Seq(recp1, recp2)), Seq(gzip(payload1), gzip(payload2)))`). -- Batching configuration now allows setting different parallelism for pruning (currently only for Sequencer pruning): - New option `canton.sequencers.sequencer.parameters.batching.pruning-parallelism` (defaults to `2`) can be used - separately from the general `canton.sequencers.sequencer.parameters.batching.parallelism` setting. -- Made the config option `...topology.use-time-proofs-to-observe-effective-time` work and changed the default to `false`. - Disabling this option activates a more robust time advancement broadcast mechanism on the sequencers, - which however still does not tolerate crashes or big gaps in block sequencing times. The parameters can be configured - in the sequencer via `canton.sequencers..parameters.time-advancing-topology`. -- Additional metrics for the ACS commitment processor: `daml.participant.sync.commitments.last-incoming-received`, `daml.participant.sync.commitments.last-incoming-processed`, `daml.participant.sync.commitments.last-locally-completed`, and `daml.participant.sync.commitments.last-locally-checkpointed`. - -## Breaking Changes - -### Removal of legacy party replication repair console macros - -The original party replication method, which relied on a silent synchronizer, has been superseded by the offline party -replication process. Consequently, the obsolete repair console macros associated with the legacy approach have -been removed. - -Specifically, the following macros are no longer available: -- `step1_hold_and_store_acs` -- `step2_import_acs` - -If you previously relied on the _Silent synchronizer replication procedure_, you will need to transition to the -current offline party replication process. For details, please consult the -[Offline Party Replication documentation](https://docs.digitalasset.com/operate/3.5/howtos/operate/parties/party_replication.html#offline-party-replication) - -### Removal of deprecated, legacy ACS export and import endpoints - -The legacy repair endpoints for the ACS export and import have been removed: - -- Console command `participant.repair.export_acs_old` -- Console command `participant.repair.import_acs_old` -- gRPC rpc `ParticipantRepairService.ExportAcsOld` -- gRPC rpc `ParticipantRepairService.ImportAcsOld` - -#### Migration advice - -Use repair endpoints without the 'old' suffix: - -- Migrate to `participant.repair.export_acs` from `participant.repair.export_acs_old` -- Migrate to `participant.repair.import_acs` from `participant.repair.import_acs_old` -- Migrate to `ParticipantRepairService.ExportAcs` from `ParticipantRepairService.ExportAcsOld` -- Migrate to `ParticipantRepairService.ImportAcs` from `ParticipantRepairService.ImportAcsOld` - -Note that previously created ACS snapshots with the legacy endpoints cannot be imported with the current endpoints as -the underlying data format has completely changed. - -##### Migrating to export_acs - -The most significant change is the removal of the `timestamp` parameter, which has been replaced by a mandatory -`ledgerOffset` parameter. - -**Console parameter changes:** - -- **New mandatory parameter:** `ledgerOffset (Long)`. You must now specify the exact ledger offset for the snapshot - instead of a `timestamp`. -- **Removed parameters:** `partiesOffboarding`, `timestamp` (replaced by `ledgerOffset`), `force`. -- **Renamed parameters:** `outputFile` is now `exportFilePath` (default is `"canton-acs-export.gz"`), - `filterSynchronizerId` is now `synchronizerId`. -- **New optional parameters:** `excludedStakeholders` allows you to omit contracts that have one or more of these - parties as a stakeholder; `contractSynchronizerRenames` allows mapping contracts from one synchronizer to another - during export. - -**gRPC changes for `ExportAcsRequest`:** - -- **`parties` -> `party_ids`:** Field renamed for consistency. If left empty, the endpoint will act as a wildcard and - export the ACS for *all* parties hosted by the participant. -- **`timestamp` -> `ledger_offset` (Breaking):** You must provide an exact `int64 ledger_offset` instead of a timestamp. -- **`filter_synchronizer_id` -> `synchronizer_id`:** Field renamed for consistency. -- **Removed fields:** `force` and `parties_offboarding` have been completely removed. -- **New fields:** `contract_synchronizer_renames` and `excluded_stakeholder_ids`. - -##### Migrating to import_acs - -The import command remains largely the same in basic usage, but introduces new optional parameters for advanced -validation and overrides, alongside strict memory-efficient streaming semantics for gRPC. - -**Console parameter changes:** - -- **Renamed parameter:** `inputFile` is now `importFilePath` (default is `"canton-acs-export.gz"`). -- **New optional parameters:** `contractImportMode` governs contract validation upon import (defaults to - `ContractImportMode.Validation`); `representativePackageIdOverride` allows overriding representative package IDs - during import; `excludedStakeholders` allows omitting contracts that have one or more of these parties as a - stakeholder. - -**gRPC changes for `ImportAcsRequest`:** - -- **Streaming Semantics (Breaking):** The new endpoint requires metadata fields (like `contract_import_mode`, - `synchronizer_id`, etc.) to be populated *only* in the first request of the stream. Subsequent requests must omit - metadata and only contain the binary `acs_snapshot` chunks. -- **New mandatory fields:** `contract_import_mode` and `synchronizer_id` must be explicitly defined in the first stream - request. -- **Removed fields:** `allow_contract_id_suffix_recomputation` is completely removed. -- **New fields:** `excluded_stakeholder_ids` and `representative_package_id_override`. -- **Response update:** `ImportAcsResponse` is now a completely empty message (previously returned a contract ID - mapping). - -### Only PackageName is accepted on Ledger API - -Usage of package id for ledger queries was deprecated and now the validation will fail if used. -The impacted APIs are: - - GetUpdates - - GetUpdateByOffset - - GetUpdateById - - GetActiveContracts - - GetEventsByContractIdRequest - - SubmitAndWaitForTransaction (the optional `transaction_format`) - - SubmitAndWaitForReassignmentRequest - - ExecuteSubmissionAndWaitForTransactionRequest - -### SynchronizerId field update in Externally signed transactions - -In Protocol version 35, the `synchronizer_id` field in externally signed prepared transaction metadata -will be populated with the physical synchronizer ID of the synchronizer on which the transaction will be processed, -instead of the logical synchronizer ID, as is the case in PV 34. -Applications must ensure they do not rely on the format of the `synchronizer_id` value. - -### Changes from NonNegativeLong to Long - -Some console commands using a NonNegativeLong for the offset are changed to accept a Long instead. -Similarly, some console commands returning an offset now return a Long instead of a NonNegativeLong. -It brings consistency and allows to pass the output of `participant.ledger_api.state.end()`. - -Impacted commands: -- `participant.repair.export_acs` -- `participant.parties.find_party_max_activation_offset` -- `participant.parties.find_party_max_deactivation_offset` -- `participant.parties.find_highest_offset_by_timestamp` - -### Removal of automatic recomputation of contract ids upon ACS import - -The ability to recompute contract ids upon ACS import has been removed. - -### Removal of multi-host name resolution tooling - -Support for the multi-host name resolution was removed. -This was only used if synchronizer connectivity defined a sequencer with multiple endpoints, which is not supported with our current sequencers: -we now have multiple sequencers each with exactly one endpoint. - -### Ledger JSON API Spec Corrections - -JSON Ledger API OpenAPI/AsyncAPI spec corrections -- Fields not marked as required in the Ledger API `.proto` specification are now also optional in the OpenAPI/AsyncAPI specifications. - If your client code is using code generated using previous versions of these specifications, it may not compile or function correctly with the new version. To migrate: - - If you prefer not to update your code, continue using the previous specification versions as the JSON API server preserves backward compatibility. - - If you want to use new endpoints, features or leverage the new less strict spec, migrate to the new OpenAPI/AsyncAPI specifications as follows: - - Java clients: No changes are needed if you use the `OpenAPI Generator`. Otherwise, potentially optionality of fields should be handled appropriately for other code generators. - - TypeScript clients: Update your code to handle optional fields, using the `!` or `??` operators as appropriate. -- From Canton 3.5 onwards, OpenAPI/AsyncAPI specification files are suffixed with the Canton version (e.g., `openapi-3.5.0.yaml`). -- Canton 3.5 is compatible with OpenAPI specification files from version 3.4.0 to 3.5.0 (inclusive). - -- The Ledger JSON API server now enforces that only fields marked as required by the Ledger API OpenAPI/AsyncAPI specification are mandatory in request payloads. - -### Change from grpcurl to grpc-health-probe in all Docker images - -The tool used for health check probes changed from grpcurl to grpc-health-probe in all the docker images. - -### Minor Breaking Changes - -- The expert `keep-alive-client` configuration parameter for various client services moved to `channel.keep-alive-client`. -- We reduced the defaults for `setBalanceRequestSubmissionWindowSize` and `defaultMaxSequencingTimeOffset` - to 2 minutes. -- The default OTLP gRPC port that the Canton connects to in order to export the traces has been changed from - 4318 to 4317. This aligns the default configuration of Canton with the default configuration of the OpenTelemetry - Collector. This change affects only the users who have configured an OTLP trace export through - ``` - canton.monitoring.tracing.tracer.exporter.type=otlp - ``` -- Removed the `LastErrorsAppender` along with the Admin API endpoints `StatusService.GetLastErrors` and `StatusServiceGetLastErrorTrace`, as - well as the corresponding console commands `last_errors` and `last_error_trace`. - - -## Deprecations - -### Deprecate scope-based access tokens -- "Scope-based" access tokens, i.e. JWTs without any audience specified, have been deprecated. -- A configuration that does not specify a `target-audience` will log a warning on node startup. -- Configurations that specify both a `target-audience` and a `target-scope` are not supported in this version and will also log a warning on node startup. -- Starting Canton version 3.7, support for "scope-based" tokens will be removed entirely to enforce a valid `aud` field in every incoming JWT. -- The `scope` field will, in a future version, be repurposed to serve exclusively as an additional, optional claim for fine-grained permissions. - -### Removal of the old sequencer connection transports - -The old sequencer connections transports have been removed, and only the new sequencer connection pool remains. -Consequently, the configuration `.sequencer-client.use-new-connection-pool` has been deprecated and no longer has any effect. - -### Deprecate initial protocol version configuration - -The config key `participant.parameters.initial-protocol-version` was unused and has been marked as deprecated. - -### Configuration Deprecations - -- The configuration parameters `topology.use-new-processor` and `topology.use-new-client` have been deprecated and now default to true. Configuring those parameters to false will be ignored. -- The parameter `canton.participants..parameters.package-metadata-view.init-takes-too-long-interval` - is now ignored, and a warning will only be printed once, rather than periodically. -- The parameter `canton.participants..parameters.ledger-api-server.indexer.prepare-package-metadata-time-out-warning` - is now ignored. -- The individual JVM metric flags `classes`, `cpu`, `memoryPools`, `threads`, `gc`, and `buffers` in - `canton.monitoring.metrics.jvm-metrics` are no longer supported since the upgrade to OpenTelemetry instrumentation 2.26.0. - All standard JVM metrics (classes, cpu, memory pools, threads, garbage collector) are now always enabled when - `jvm-metrics.enabled = true`. A new `experimental` flag has been added to control experimental JVM metrics - (e.g. buffer pools). Users who previously set `buffers = true` should migrate to `experimental = true`. - See https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/16087 for details. -- The Zipkin trace exporter configuration `canton.monitoring.tracing.tracer.exporter.type=zipkin` is - deprecated following the OpenTelemetry specification deprecation of Zipkin exporters. The Zipkin exporter - will be removed in a future release. Users should migrate to the OTLP exporter. - See https://opentelemetry.io/blog/2025/deprecating-zipkin-exporters/ for details. -- Removed the feature flag `canton.sequencers..parameters.async-writer.enabled`, as async writing is now - the only supported mode. -- Changed the path for `crypto.kms.session-signing-keys` (deprecated) to `crypto.session-signing-keys` so that session signing key configuration is no longer directly tied to a KMS. However, session signing keys can still only be enabled when using a KMS provider or when running with `non-standard-config=true`. -- `package-dependency-cache` field in `caching` configuration is deprecated. It can be removed safely from node configurations. - -### Ledger JSON API package vetting endpoints - -The Ledger JSON API `v2/package-vetting` endpoint exposes list functionality on the GET method by accepting a request body. This is not recommended by the HTTP specification, hence the endpoint is deprecated. -For consistency, the POST method, used for updating the vetting state, of the same endpoint is also deprecated. - -In turn, two new endpoints are implemented to provide the same functionality: -- `v2/package-vetting/list` accepts a POST request with the same body as the deprecated GET `v2/package-vetting` endpoint and returns the list of vetted packages in the same format. -- `v2/package-vetting/update` accepts a POST request with the same body as the deprecated POST endpoint `v2/package-vetting` and returns the updated vetting state of the package in the same format. - - -### Protocol version parameter in topology list commands - -The `protocolVersion` parameter in all `.topology..list` console commands has been deprecated and will be removed in a future version. - -## Minor Improvements - -### Bugfixes - -- Fixed a mid-crash recovery issue for offline party replication and repair ACS imports. Previously, if an ACS import - was interrupted (for example by a participant node restart or crash), a subsequent recovery attempt could result in - missing contracts on the Ledger API. The recovery process now properly rolls back uncommitted partial states upon - retrying the ACS import, ensuring recovered contracts are completely synchronized across both internal storage - and the Ledger API. -- Fixed a bug where the Ledger API `PackageService.ListVettedPackages` used to return a potentially not yet - effective state of the vetted packages. Now it returns the state of vetted packages effective at the time of the request. -- Sequencer health status used to incorrectly return the synchronizer uid instead of the sequencer uid. -- Prevent Ledger API crashes after running `ParticipantRepairService.PurgeContracts` admin command. - Fixes a critical issue where using the `ParticipantRepairService.PurgeContracts` command (when multi-synchronizer support is disabled) generated malformed - Daml values for the choice argument and choice result of the `Archive` choice of the purge contract events in the Ledger API event store. - This previously caused the Ledger API streams reading the generated `Archive` events to crash. - The repair command now generates correct Daml values for the corresponding entries, that can be safely delivered by the Ledger API. -- Fixed a bug in the repair service's `changeAssignation` where only a single repair counter was allocated when reassigning multiple contracts, - violating the monotonicity expected by the indexer. - -### Ledger API Multi-Synchronizer Events Alpha Support - -Adds a new participant node parameter, `alpha-multi-synchronizer-support` (Boolean). -- **Default (`false`):** Uses standard **Create** and **Archive** events. -- **Enabled (`true`):** Uses **Assign** and **Unassign** events. - -This flag is required in multi-synchronizer environments to preserve the **reassignment counter** of a contract. -Using the default (Create events) resets this counter to zero. - -Note: Multi-synchronizer support is currently in Alpha; most Ledger API consumers may not yet be compatible with -Assign/Unassign events. Only enable this if your application specifically requires non-zero reassignment counters -and can process these event types. - -### Support for adding table settings for PostgreSQL - -Added support for adding table settings for PostgreSQL. One can use a repeatable migration (Flyway feature) in a file -provided to Canton externally. - - Use the new config `repeatable-migrations-paths` under the `canton...storage.parameters` configuration section. - - The config takes a list of directories where repeatable migration files must be placed, paths must be prefixed with `filesystem:` for Flyway to recognize them. - - Example: `canton.sequencers.sequencer1.storage.parameters.repeatable-migrations-paths = ["filesystem:community/common/src/test/resources/test_table_settings"]`. - - Only repeatable migrations are allowed in these directories: files with names starting with `R__` and ending with `.sql`. - - The files cannot be removed once added, but they can be modified (unlike the `V__` versioned schema migrations), and if modified these will be reapplied on each Canton startup. - - The files are applied in lexicographical order. - - Example use case: adding `autovacuum_*` settings to existing tables. - - Only add idempotent changes in repeatable migrations. - -### Offline root namespace key scripts - -Offline root namespace key scripts: -- Renamed `prepare-certs.sh` to `prepare-cert.sh` -- Changed `assemble-certs.sh` to automatically suffix the generated certificate with a `.cert` extension, similarly to what is being done in `prepare-cert.sh` -- Removed the `10-offline-root-namespace-init` example folder as its content is now integrated in the documented how-to: https://docs.digitalasset.com/operate/3.5/howtos/secure/keys/namespace_key.html -- Committed the buf image necessary to run the script to the repository (also available in the release artifact), making usage from the open source repo easier - -### Reliability Improvements - -- Added a field `MaxConcurrentCallsPerConnection` and corresponding default - `defaultMaxConcurrentCallsPerConnection` (set to 100000) to `ServerConfig`. - This corresponds to `max-concurrent-streams-per-connection` in the app configs, e.g., - `docker/canton/images/canton-sequencer/app.conf` and can be changed there. At present - the value for sequencers is configured to be 500 for the public API and 100 for the Admin API. -- Added network timeout and client_connection_check_interval for db operations in the Ledger API server and indexer to avoid - hanging connections for Postgres (see PostgresDataSourceConfig). The defaults are 60 seconds network timeout and - 5 seconds client_connection_check_interval for the Ledger API server, and 20 seconds network timeout and - 5 seconds client_connection_check_interval for the indexer. These values can be configured via the new configuration parameters - `canton.participants..ledger-api.postgres-data-source.network-timeout` for network timeout of the Ledger API - server and `canton.participants..parameters.ledger-api-server.indexer.postgres-data-source.client-connection-check-interval` - for the client_connection_check_interval of the indexer. -- `.replication.connection-pool.connection.client-connection-check-interval` is introduced - that allows configuring the PostgreSQL-specific `client_connection_check_interval` parameter for DB locked connections. - This is a safety mechanism to prevent hanging connections in case of network issues. The default value is 5 seconds. -- The Ledger API now enforces a maximum number of signatures per party that can be provided for external submissions. - This value defaults to 50 and can be changed at the following config path: `canton.participants..ledger-api.interactive-submission-service.maximum-number-of-signatures-per-party` -- Added a new configuration parameter `canton.participants..ledger-api.index-service.max-lookup-limit` that caps the maximum number of contracts returned by a contract key lookup per request. - The default value is 1000. -- When the AcsCommitmentProcessor is initializing, read stakeholder groups from the snapshot in batches of size - `canton.parameters.general.batching.max-stakeholder-groups-batch-size` (default 1000), rather than all at once. - This allows early termination of this initialization if the node is shutting down. -- The release version is now exposed in `NodeStatus.NotInitialized`, so the node version can be retrieved even before the node is initialized. +#### Recommendation ## Compatibility @@ -850,4 +74,7 @@ Canton has been tested against the following versions of its dependencies: | Postgres | POSTGRES_VERSION | +## What's Coming + +We are currently working on diff --git a/canton/VERSION b/canton/VERSION index 4bba389fca..50ebb4c654 100644 --- a/canton/VERSION +++ b/canton/VERSION @@ -1 +1 @@ -3.5.1-SNAPSHOT +3.5.7-SNAPSHOT diff --git a/canton/base/testing-utils/src/main/resources/logback-test.xml b/canton/base/testing-utils/src/main/resources/logback-test.xml new file mode 100644 index 0000000000..bab07ca00c --- /dev/null +++ b/canton/base/testing-utils/src/main/resources/logback-test.xml @@ -0,0 +1,61 @@ + + + + + true + + + + + + %highlight(%-5level %logger{10} %replace(tid:%mdc{trace-id} ){'tid: ', ''}- %msg%replace(, context: %marker){', context: $', ''}%replace( err-context:%mdc{err-context} ){' err-context: ', ''}%n) + + + WARN + + + + + ${LOG_FILE_NAME:-log/canton_test.log} + ${LOG_APPEND:-true} + + + %date [%thread] %-5level %logger{10} %replace(tid:%mdc{trace-id} ){'tid: ', ''}- %msg%replace(, context: %marker){', context: $', ''}%replace( err-context:%mdc{err-context} ){' err-context: ', ''}%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala b/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala index 6ae3d9fc47..81820186d6 100644 --- a/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala +++ b/canton/base/testing-utils/src/main/scala/com/daml/testing/utils/GrpcServerResource.scala @@ -6,6 +6,7 @@ package com.daml.testing.utils import io.grpc.* import java.net.SocketAddress +import java.util.UUID import java.util.concurrent.TimeUnit final class GrpcServerResource( @@ -17,7 +18,11 @@ final class GrpcServerResource( override protected def construct(): ServerWithChannelProvider = { boundServices = services() - ServerWithChannelProvider.fromServices(boundServices, port, "server") + ServerWithChannelProvider.fromServices( + boundServices, + port, + s"server_${UUID.randomUUID().toString}", + ) } override protected def destruct(resource: ServerWithChannelProvider): Unit = { diff --git a/canton/build.sbt b/canton/build.sbt index 2db3b9fdad..8bc1022332 100644 --- a/canton/build.sbt +++ b/canton/build.sbt @@ -464,6 +464,10 @@ lazy val `model-based-testing-drivers` = CommunityProjects.`model-based-testing-drivers` lazy val `model-based-testing-integration-tests` = CommunityProjects.`model-based-testing-integration-tests` +lazy val `traffic-enforcement-api` = + CommunityProjects.`traffic-enforcement-api` +lazy val `traffic-enforcement-component` = + CommunityProjects.`traffic-enforcement-component` lazy val `scalatest-utils` = DamlProjects.`scalatest-utils` lazy val `scala-utils` = DamlProjects.`scala-utils` diff --git a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto index eba2bad55c..ded3845a12 100644 --- a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto +++ b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/participant/v30/synchronizer_connectivity_service.proto @@ -7,6 +7,7 @@ package com.digitalasset.canton.admin.participant.v30; import "com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.proto"; import "com/digitalasset/canton/admin/time/v30/time_tracker_config.proto"; +import "com/digitalasset/canton/admin/topology/v30/common.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; @@ -123,13 +124,44 @@ message ModifySynchronizerRequest { message ModifySynchronizerResponse {} -message ListRegisteredSynchronizersRequest {} +message ListRegisteredSynchronizersRequest { + // If false (default) returns only active synchronizers. Otherwise, return all synchronizers. + bool all_statuses = 1; +} message ListRegisteredSynchronizersResponse { + enum Status { + STATUS_UNSPECIFIED = 0; + + // The node can connect to the synchronizer. + // At most one connection per alias/synchronizer id can be active. + STATUS_ACTIVE = 1; + + // Source and target synchronizer in the context of hard domain migration. + // The node will refuse to connect to a synchronizer with these statuses. + STATUS_HARD_MIGRATING_SOURCE = 2; + STATUS_HARD_MIGRATING_TARGET = 3; + + // Around LSU, the connection of the successor is initially stored with status LSU target. + // When upgrade time is reached and the participant node performs the LSU, then: + // 1. The status of the current synchronizer is changed from Active to LSU source. + // 2. The status of the successor is changes from LSU target to Active. + // The node will refuse to connect to a synchronizer with these statuses. + STATUS_LSU_SOURCE = 4; + STATUS_LSU_TARGET = 5; + + // Currently only used in the context of hard domain migration. + // The old synchronizer connection is marked as Inactive at the end of the migration. + // The node will refuse to connect to a synchronizer with this status. + STATUS_INACTIVE = 6; + } + message Result { SynchronizerConnectionConfig config = 1; bool connected = 2; optional string physical_synchronizer_id = 3; + Status status = 4; + optional com.digitalasset.canton.admin.topology.v30.SynchronizerPredecessor synchronizer_predecessor = 5; } repeated Result results = 1; } diff --git a/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto new file mode 100644 index 0000000000..67bf0b5735 --- /dev/null +++ b/canton/community/admin-api/src/main/protobuf/com/digitalasset/canton/admin/topology/v30/common.proto @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package com.digitalasset.canton.admin.topology.v30; + +import "google/protobuf/timestamp.proto"; + +message SynchronizerPredecessor { + // Id of the predecessor. + string predecessor_physical_id = 1; + + // When the upgrade happened/is supposed to happen. + google.protobuf.Timestamp upgrade_time = 2; + + // Whether the participant is performing a late synchronizer upgrade. + // default: false + bool is_late_upgrade = 3; +} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala index 43e4cc7e53..b6918bbdd5 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/LedgerApiCommands.scala @@ -190,10 +190,18 @@ import com.digitalasset.canton.networking.grpc.ForwardingStreamObserver import com.digitalasset.canton.platform.apiserver.execution.CommandStatus import com.digitalasset.canton.protocol.LfContractId import com.digitalasset.canton.serialization.ProtoConverter +import com.digitalasset.canton.tea.v1.TrafficServiceGrpc.TrafficServiceStub +import com.digitalasset.canton.tea.v1.{ + GetAccountRequest, + GetAccountResponse, + TrafficServiceGrpc, + UpdateAccountRequest, + UpdateAccountResponse, +} import com.digitalasset.canton.topology.transaction.TopologyTransaction.GenericTopologyTransaction import com.digitalasset.canton.topology.{ParticipantId, Party, PartyId, SynchronizerId} import com.digitalasset.canton.util.BinaryFileUtil -import com.digitalasset.canton.{LfPackageId, LfPackageName, LfPartyId} +import com.digitalasset.canton.{LfPackageId, LfPackageName, LfPartyId, config} import com.google.protobuf.empty.Empty import com.google.protobuf.field_mask.FieldMask import io.grpc.* @@ -1747,6 +1755,7 @@ object LedgerApiCommands { minLedgerTimeAbs: Option[Instant], deduplicationPeriod: Option[DeduplicationPeriod], hashingSchemeVersion: HashingSchemeVersion, + optTimeout: Option[config.NonNegativeDuration], ) extends BaseCommand[ ExecuteSubmissionAndWaitRequest, ExecuteSubmissionAndWaitResponse, @@ -1776,7 +1785,8 @@ object LedgerApiCommands { ): Either[String, ExecuteSubmissionAndWaitResponse] = Right(response) - override def timeoutType: TimeoutType = DefaultUnboundedTimeout + override def timeoutType: TimeoutType = + optTimeout.map(CustomClientTimeout(_)).getOrElse(DefaultUnboundedTimeout) } final case class ExecuteAndWaitForTransactionCommand( @@ -1790,6 +1800,7 @@ object LedgerApiCommands { transactionShape: Option[TransactionShape], includeCreatedEventBlob: Boolean, customEventFormat: Option[EventFormat], + optTimeout: Option[config.NonNegativeDuration], ) extends BaseCommand[ ExecuteSubmissionAndWaitForTransactionRequest, ExecuteSubmissionAndWaitForTransactionResponse, @@ -1851,7 +1862,8 @@ object LedgerApiCommands { ): Either[String, ExecuteSubmissionAndWaitForTransactionResponse] = Right(response) - override def timeoutType: TimeoutType = DefaultUnboundedTimeout + override def timeoutType: TimeoutType = + optTimeout.map(CustomClientTimeout(_)).getOrElse(DefaultUnboundedTimeout) } final case class PreferredPackageVersion( @@ -2450,4 +2462,48 @@ object LedgerApiCommands { } } + object Traffic { + + abstract class BaseCommand[Req, Res] extends GrpcAdminCommand[Req, Res, Res] { + override type Svc = TrafficServiceStub + + override def createService(channel: ManagedChannel): TrafficServiceStub = + TrafficServiceGrpc.stub(channel) + + override protected def handleResponse(response: Res): Either[String, Res] = Right(response) + } + + final case class GetAccount(accountId: String) + extends BaseCommand[ + GetAccountRequest, + GetAccountResponse, + ] { + override protected def createRequest(): Either[String, GetAccountRequest] = + Right(GetAccountRequest(accountId)) + + override protected def submitRequest( + service: TrafficServiceStub, + request: GetAccountRequest, + ): Future[GetAccountResponse] = + service.getAccount(request) + } + + final case class UpdateAccount( + accountId: String, + balance: Option[Long], + deduplicationId: String, + ) extends BaseCommand[ + UpdateAccountRequest, + UpdateAccountResponse, + ] { + override protected def createRequest(): Either[String, UpdateAccountRequest] = + Right(UpdateAccountRequest(accountId, balance, deduplicationId)) + + override protected def submitRequest( + service: TrafficServiceStub, + request: UpdateAccountRequest, + ): Future[UpdateAccountResponse] = + service.updateAccount(request) + } + } } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala index 1c1b6208f9..5ceb4f63ee 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/commands/ParticipantAdminCommands.scala @@ -13,6 +13,12 @@ import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{ } import com.digitalasset.canton.admin.api.client.data as admin import com.digitalasset.canton.admin.api.client.data.PackageDescription.PackageContents +import com.digitalasset.canton.admin.api.client.data.{ + ConfiguredPhysicalSynchronizerId, + RegisteredSynchronizer, + SynchronizerConnectionConfig, + SynchronizerPredecessor, +} import com.digitalasset.canton.admin.participant.v30 import com.digitalasset.canton.admin.participant.v30.PackageServiceGrpc.PackageServiceStub import com.digitalasset.canton.admin.participant.v30.ParticipantInspectionServiceGrpc.ParticipantInspectionServiceStub @@ -48,7 +54,7 @@ import com.digitalasset.canton.participant.pruning.AcsCommitmentProcessor.{ ReceivedCmtState, SentCmtState, } -import com.digitalasset.canton.participant.synchronizer.SynchronizerConnectionConfig +import com.digitalasset.canton.participant.synchronizer.SynchronizerConnectionConfig as InternalSynchronizerConnectionConfig import com.digitalasset.canton.protocol.LfContractId import com.digitalasset.canton.protocol.messages.{AcsCommitment, CommitmentPeriod} import com.digitalasset.canton.scheduler.SafeToPruneCommitmentState @@ -58,7 +64,6 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.time.PositiveSeconds import com.digitalasset.canton.topology.transaction.{GrpcConnection, ParticipantPermission} import com.digitalasset.canton.topology.{ - ConfiguredPhysicalSynchronizerId, ParticipantId, PartyId, PhysicalSynchronizerId, @@ -937,7 +942,7 @@ object ParticipantAdminCommands { final case class MigrateSynchronizer( sourceSynchronizerAlias: SynchronizerAlias, - targetSynchronizerConfig: SynchronizerConnectionConfig, + targetSynchronizerConfig: InternalSynchronizerConnectionConfig, force: Boolean, ) extends GrpcAdminCommand[ v30.MigrateSynchronizerRequest, @@ -1149,7 +1154,7 @@ object ParticipantAdminCommands { currentPsid: PhysicalSynchronizerId, successorPsid: PhysicalSynchronizerId, upgradeTime: CantonTimestamp, - successorConfig: SynchronizerConnectionConfig, + successorConfig: InternalSynchronizerConnectionConfig, sequencerConnectionValidation: SequencerConnectionValidation, ) extends GrpcAdminCommand[ v30.PerformLateLsuRequest, @@ -1383,7 +1388,7 @@ object ParticipantAdminCommands { } - final case object ListRegisteredSynchronizers + final case object ListActiveRegisteredSynchronizers extends Base[ v30.ListRegisteredSynchronizersRequest, v30.ListRegisteredSynchronizersResponse, @@ -1395,7 +1400,7 @@ object ParticipantAdminCommands { override protected def createRequest() : Either[String, v30.ListRegisteredSynchronizersRequest] = Right( - v30.ListRegisteredSynchronizersRequest() + v30.ListRegisteredSynchronizersRequest(allStatuses = false) ) override protected def submitRequest( @@ -1418,21 +1423,75 @@ object ParticipantAdminCommands { ] = for { configP <- result.config.toRight("Server has sent empty config") - config <- SynchronizerConnectionConfig.fromProtoV30(configP).leftMap(_.toString) + config <- InternalSynchronizerConnectionConfig.fromProtoV30(configP).leftMap(_.toString) psid <- result.physicalSynchronizerId .traverse( PhysicalSynchronizerId.fromProtoPrimitive(_, "physical_synchronizer_id") ) .map(ConfiguredPhysicalSynchronizerId(_)) .leftMap(_.toString) - } yield (config, psid, result.connected) + } yield (SynchronizerConnectionConfig.fromInternal(config), psid, result.connected) + + response.results.traverse(mapRes) + } + } + + // All registered synchronziers (including inactive ones) + final case object ListAllRegisteredSynchronizers + extends Base[ + v30.ListRegisteredSynchronizersRequest, + v30.ListRegisteredSynchronizersResponse, + Seq[RegisteredSynchronizer], + ] { + + override protected def createRequest() + : Either[String, v30.ListRegisteredSynchronizersRequest] = + Right( + v30.ListRegisteredSynchronizersRequest(allStatuses = true) + ) + + override protected def submitRequest( + service: SynchronizerConnectivityServiceStub, + request: v30.ListRegisteredSynchronizersRequest, + ): Future[v30.ListRegisteredSynchronizersResponse] = + service.listRegisteredSynchronizers(request) + + override protected def handleResponse( + response: v30.ListRegisteredSynchronizersResponse + ): Either[String, Seq[RegisteredSynchronizer]] = { + + def mapRes( + result: v30.ListRegisteredSynchronizersResponse.Result + ): Either[String, RegisteredSynchronizer] = + for { + configP <- result.config.toRight("Server has sent empty config") + config <- InternalSynchronizerConnectionConfig.fromProtoV30(configP).leftMap(_.toString) + psid <- result.physicalSynchronizerId + .traverse( + PhysicalSynchronizerId.fromProtoPrimitive(_, "physical_synchronizer_id") + ) + .map(ConfiguredPhysicalSynchronizerId(_)) + .leftMap(_.toString) + + predecessor <- result.synchronizerPredecessor + .traverse(SynchronizerPredecessor.fromProtoV30) + .leftMap(_.toString) + + status <- RegisteredSynchronizer.Status.fromProtoV30(result.status).leftMap(_.toString) + } yield RegisteredSynchronizer( + config = SynchronizerConnectionConfig.fromInternal(config), + status = status, + psid = psid, + predecessor = predecessor, + isConnected = result.connected, + ) response.results.traverse(mapRes) } } final case class ConnectSynchronizer( - config: SynchronizerConnectionConfig, + config: InternalSynchronizerConnectionConfig, sequencerConnectionValidation: SequencerConnectionValidation, ) extends Base[v30.ConnectSynchronizerRequest, v30.ConnectSynchronizerResponse, Unit] { @@ -1460,7 +1519,7 @@ object ParticipantAdminCommands { } final case class RegisterSynchronizer( - config: SynchronizerConnectionConfig, + config: InternalSynchronizerConnectionConfig, performHandshake: Boolean, sequencerConnectionValidation: SequencerConnectionValidation, ) extends Base[v30.RegisterSynchronizerRequest, v30.RegisterSynchronizerResponse, Unit] { @@ -1497,7 +1556,7 @@ object ParticipantAdminCommands { final case class ModifySynchronizerConnection( synchronizerId: Option[PhysicalSynchronizerId], - config: SynchronizerConnectionConfig, + config: InternalSynchronizerConnectionConfig, sequencerConnectionValidation: SequencerConnectionValidation, ) extends Base[v30.ModifySynchronizerRequest, v30.ModifySynchronizerResponse, Unit] { @@ -1574,7 +1633,7 @@ object ParticipantAdminCommands { ), newConfig => v30.PerformManualLsuRequest.SuccessorConnectionConfiguration.Config( - newConfig.toProtoV30 + newConfig.toInternal.toProtoV30 ), ) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala new file mode 100644 index 0000000000..c09e3d9079 --- /dev/null +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/RegisteredSynchronizer.scala @@ -0,0 +1,107 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.admin.api.client.data + +import cats.syntax.either.* +import com.digitalasset.canton.ProtoDeserializationError +import com.digitalasset.canton.ProtoDeserializationError.UnrecognizedEnum +import com.digitalasset.canton.admin.api.client.data.RegisteredSynchronizer.Status +import com.digitalasset.canton.admin.participant.v30.ListRegisteredSynchronizersResponse.Status as StatusV30 +import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult +import com.digitalasset.canton.topology.PhysicalSynchronizerId + +/** Information about a registered synchronizer + * @param config + * Configuration to connect to the synchronizer + * @param status + * Status of the connection (e.g., Active, Inactive, LSU) + * @param psid + * If known, the physical synchronizer id of the synchronizer. Always known after the first + * connection. + * @param predecessor + * If set, the LSU predecessor. + * @param isConnected + * Whether the node is currently connected to the synchronizer + */ +final case class RegisteredSynchronizer( + config: SynchronizerConnectionConfig, + status: Status, + psid: ConfiguredPhysicalSynchronizerId, + predecessor: Option[SynchronizerPredecessor], + isConnected: Boolean, +) + +sealed trait ConfiguredPhysicalSynchronizerId extends PrettyPrinting { + def toOption: Option[PhysicalSynchronizerId] + def isDefined: Boolean = toOption.isDefined +} + +object ConfiguredPhysicalSynchronizerId { + def apply(psid: Option[PhysicalSynchronizerId]): ConfiguredPhysicalSynchronizerId = + psid.fold[ConfiguredPhysicalSynchronizerId](UnknownPhysicalSynchronizerId)( + KnownPhysicalSynchronizerId(_) + ) +} + +final case class KnownPhysicalSynchronizerId(psid: PhysicalSynchronizerId) + extends ConfiguredPhysicalSynchronizerId { + override protected def pretty: Pretty[KnownPhysicalSynchronizerId] = + prettyOfString(psid => psid.psid.toLengthLimitedString.unwrap) + + override def toOption: Option[PhysicalSynchronizerId] = Some(psid) +} +case object UnknownPhysicalSynchronizerId extends ConfiguredPhysicalSynchronizerId { + override protected def pretty: Pretty[UnknownPhysicalSynchronizerId.type] = + prettyOfString(_ => "UnknownPhysicalSynchronizerId") + + override def toOption: Option[PhysicalSynchronizerId] = None +} + +object RegisteredSynchronizer { + sealed trait Status extends Serializable with Product with PrettyPrinting + + object Status { + def fromProtoV30(proto: StatusV30): ParsingResult[Status] = + proto match { + case StatusV30.STATUS_UNSPECIFIED => ProtoDeserializationError.FieldNotSet("status").asLeft + case StatusV30.STATUS_ACTIVE => Active.asRight + case StatusV30.STATUS_HARD_MIGRATING_SOURCE => HardMigratingSource.asRight + case StatusV30.STATUS_HARD_MIGRATING_TARGET => HardMigratingTarget.asRight + case StatusV30.STATUS_LSU_SOURCE => LsuSource.asRight + case StatusV30.STATUS_LSU_TARGET => LsuTarget.asRight + case StatusV30.STATUS_INACTIVE => Inactive.asRight + case StatusV30.Unrecognized(unrecognizedValue) => + UnrecognizedEnum("status", unrecognizedValue).asLeft + } + + case object Active extends Status { + override protected def pretty: Pretty[Active.type] = prettyOfString(_ => "Active") + } + + case object HardMigratingSource extends Status { + override protected def pretty: Pretty[HardMigratingSource.type] = + prettyOfString(_ => "HardMigratingSource") + } + case object HardMigratingTarget extends Status { + override protected def pretty: Pretty[HardMigratingTarget.type] = + prettyOfString(_ => "HardMigratingTarget") + } + + case object LsuSource extends Status { + override protected def pretty: Pretty[LsuSource.type] = + prettyOfString(_ => "LSU source") + } + + case object LsuTarget extends Status { + override protected def pretty: Pretty[LsuTarget.type] = + prettyOfString(_ => "LSU target") + } + + case object Inactive extends Status { + override protected def pretty: Pretty[Inactive.type] = prettyOfString(_ => "Inactive") + } + } + +} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala index d06c33f095..d31e830e39 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SequencerConnections.scala @@ -119,9 +119,7 @@ final case class SequencerConnections( param("sequencer connection pool delays", _.sequencerConnectionPoolDelays), ) - private[canton] def toInternal(implicit - consoleEnvironment: ConsoleEnvironment - ): SequencerConnectionsInternal = + private[canton] def toInternal: SequencerConnectionsInternal = SequencerConnectionsInternal .many( aliasToConnection.toSeq.map { case (_, connection) => connection.toInternal }, @@ -130,17 +128,15 @@ final case class SequencerConnections( submissionRequestAmplification.toInternal, sequencerConnectionPoolDelays.toInternal, ) - .valueOr(consoleEnvironment.raiseError) + .valueOr(err => throw new IllegalArgumentException(s"Invariants not respected: $err")) } object SequencerConnections { - implicit private[data] def sequencerConnectionsToInternalTransformer(implicit - consoleEnvironment: ConsoleEnvironment - ): Transformer[SequencerConnections, SequencerConnectionsInternal] = _.toInternal + implicit private[data] def sequencerConnectionsToInternalTransformer + : Transformer[SequencerConnections, SequencerConnectionsInternal] = _.toInternal - implicit private[data] def sequencerConnectionsFromInternalTransformer(implicit - consoleEnvironment: ConsoleEnvironment - ): Transformer[SequencerConnectionsInternal, SequencerConnections] = fromInternal(_) + implicit private[data] def sequencerConnectionsFromInternalTransformer + : Transformer[SequencerConnectionsInternal, SequencerConnections] = fromInternal(_) def single( connection: SequencerConnection @@ -183,14 +179,14 @@ object SequencerConnections { } yield sequencerConnections } - def tryMany( + def many( connections: Seq[SequencerConnection], sequencerTrustThreshold: PositiveInt, sequencerLivenessMargin: NonNegativeInt, submissionRequestAmplification: SubmissionRequestAmplification, sequencerConnectionPoolDelays: SequencerConnectionPoolDelays, - )(implicit consoleEnvironment: ConsoleEnvironment): SequencerConnections = { - val resultE = for { + ): Either[String, SequencerConnections] = + for { connectionsNE <- NonEmpty.from(connections).toRight("connections should not be empty") result <- many( connectionsNE, @@ -201,14 +197,24 @@ object SequencerConnections { ) } yield result - resultE.valueOr(consoleEnvironment.raiseError) - } - - private[canton] def fromInternal(internal: SequencerConnectionsInternal)(implicit - consoleEnvironment: ConsoleEnvironment - ): SequencerConnections = + def tryMany( + connections: Seq[SequencerConnection], + sequencerTrustThreshold: PositiveInt, + sequencerLivenessMargin: NonNegativeInt, + submissionRequestAmplification: SubmissionRequestAmplification, + sequencerConnectionPoolDelays: SequencerConnectionPoolDelays, + )(implicit consoleEnvironment: ConsoleEnvironment): SequencerConnections = + many( + connections, + sequencerTrustThreshold, + sequencerLivenessMargin, + submissionRequestAmplification, + sequencerConnectionPoolDelays, + ).valueOr(consoleEnvironment.raiseError) + + private[canton] def fromInternal(internal: SequencerConnectionsInternal): SequencerConnections = SequencerConnections - .tryMany( + .many( internal.aliasToConnection.toSeq.map { case (_, connection) => SequencerConnection.fromInternal(connection) }, @@ -217,6 +223,11 @@ object SequencerConnections { SubmissionRequestAmplification.fromInternal(internal.submissionRequestAmplification), SequencerConnectionPoolDelays.fromInternal(internal.sequencerConnectionPoolDelays), ) + .valueOr(err => + throw new IllegalStateException( + s"Invariants of the sequencers connections don't hold: $err" + ) + ) } sealed trait SequencerConnectionValidation { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala index 99d20f4ce6..c2e68a5f9c 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/SynchronizerConnectionConfig.scala @@ -242,10 +242,11 @@ final case class SynchronizerConnectionConfig( ), ) - private[canton] def toInternal(implicit - consoleEnvironment: ConsoleEnvironment - ): SynchronizerConnectionConfigInternal = - this.transformInto[SynchronizerConnectionConfigInternal] + private[canton] def toInternal: SynchronizerConnectionConfigInternal = + this + .into[SynchronizerConnectionConfigInternal] + .withFieldRenamed(_.synchronizerId, _.psid) + .transform } object SynchronizerConnectionConfig { @@ -319,8 +320,8 @@ object SynchronizerConnectionConfig { ) } - private[canton] def fromInternal(internal: SynchronizerConnectionConfigInternal)(implicit - consoleEnvironment: ConsoleEnvironment + private[canton] def fromInternal( + internal: SynchronizerConnectionConfigInternal ): SynchronizerConnectionConfig = - internal.transformInto[SynchronizerConnectionConfig] + internal.into[SynchronizerConnectionConfig].withFieldRenamed(_.psid, _.synchronizerId).transform } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala index 3cba8bb352..35c0d41cca 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/admin/api/client/data/Topology.scala @@ -6,6 +6,8 @@ package com.digitalasset.canton.admin.api.client.data import cats.syntax.traverse.* import com.digitalasset.canton.admin.api.client.data.ListPartiesResult.ParticipantSynchronizers import com.digitalasset.canton.crypto.* +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.admin.v30 @@ -88,3 +90,30 @@ object ListKeyOwnersResult { encryptionKeys <- value.encryptionKeys.traverse(EncryptionPublicKey.fromProtoV30) } yield ListKeyOwnersResult(synchronizerId, owner, signingKeys, encryptionKeys) } + +final case class SynchronizerPredecessor( + psid: PhysicalSynchronizerId, + upgradeTime: CantonTimestamp, + isLateUpgrade: Boolean, +) + +object SynchronizerPredecessor { + def fromProtoV30( + proto: com.digitalasset.canton.admin.topology.v30.SynchronizerPredecessor + ): ParsingResult[SynchronizerPredecessor] = + for { + psid <- PhysicalSynchronizerId.fromProtoPrimitive( + proto.predecessorPhysicalId, + "predecessor_physical_id", + ) + upgradeTime <- ProtoConverter.parseRequired( + CantonTimestamp.fromProtoTimestamp, + "upgrade_time", + proto.upgradeTime, + ) + } yield SynchronizerPredecessor( + psid = psid, + upgradeTime = upgradeTime, + isLateUpgrade = proto.isLateUpgrade, + ) +} diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala index 087e55572d..34074b73b0 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/config/CantonConfig.scala @@ -70,6 +70,8 @@ import com.digitalasset.canton.platform.config.{ InteractiveSubmissionServiceConfig, StateServiceConfig, TopologyAwarePackageSelectionConfig, + TrafficEnforcementConfig, + TrafficEnforcementServerConfig, UpdateServiceConfig, } import com.digitalasset.canton.pureconfigutils.SharedConfigReaders.catchConvertError @@ -84,6 +86,7 @@ import com.digitalasset.canton.synchronizer.block.{SequencerDriver, SequencerDri import com.digitalasset.canton.synchronizer.config.{DeclarativeSequencerConfig, PublicServerConfig} import com.digitalasset.canton.synchronizer.mediator.{ DeduplicationStoreConfig, + DelayedVerdictSenderConfig, MediatorConfig, MediatorNodeConfig, MediatorNodeParameterConfig, @@ -99,6 +102,7 @@ import com.digitalasset.canton.synchronizer.sequencer.SequencerConfig.{ import com.digitalasset.canton.synchronizer.sequencer.block.DriverBlockSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.canton.sequencing.BftSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig +import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig.SequencerCoreSubscriptionConfig import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.framework.data.topology.BlacklistLeaderSelectionPolicyConfig import com.digitalasset.canton.synchronizer.sequencer.config.{ AsyncWriterConfig, @@ -384,38 +388,24 @@ final case class CantonFeatures( ++ (if (enableRepairCommands) Seq(FeatureFlag.Repair) else Seq())).toSet } -/** Root configuration parameters for a single Canton process. +/** The commonality between [[CantonConfig]] and `SpliceConfig` in the Splice repo, so that either + * Canton or Splice can initialize Canton components that require these configuration elements. * - * @param participants - * All locally running participants that this Canton process can connect and operate on. - * @param remoteParticipants - * All remotely running participants to which the console can connect and operate on. - * @param sequencers - * All locally running sequencers that this Canton process can connect and operate on. - * @param remoteSequencers - * All remotely running sequencers that this Canton process can connect and operate on. - * @param mediators - * All locally running mediators that this Canton process can connect and operate on. - * @param remoteMediators - * All remotely running mediators that this Canton process can connect and operate on. - * @param monitoring - * determines how this Canton process can be monitored - * @param parameters - * per-environment parameters to control enabled features and set testing parameters - * @param features - * control which features are enabled + * Prefer accepting this type over [[CantonConfig]] at least within app-base, environment setup, + * and integration test framework code. `SharedCantonConfig[?]` is frequently a specific-enough + * type for a function or class argument. */ -final case class CantonConfig( - sequencers: Map[InstanceName, SequencerNodeConfig] = Map.empty, - mediators: Map[InstanceName, MediatorNodeConfig] = Map.empty, - participants: Map[InstanceName, ParticipantNodeConfig] = Map.empty, - remoteSequencers: Map[InstanceName, RemoteSequencerConfig] = Map.empty, - remoteMediators: Map[InstanceName, RemoteMediatorConfig] = Map.empty, - remoteParticipants: Map[InstanceName, RemoteParticipantConfig] = Map.empty, - monitoring: MonitoringConfig = MonitoringConfig(), - parameters: CantonParameters = CantonParameters(), - features: CantonFeatures = CantonFeatures(), -) extends ConfigDefaults[Option[DefaultPorts], CantonConfig] { +trait SharedCantonConfig[Self] extends ConfigDefaults[Option[DefaultPorts], Self] { self: Self => + def portDescription: String + def sequencers: Map[InstanceName, SequencerNodeConfig] + def mediators: Map[InstanceName, MediatorNodeConfig] + def participants: Map[InstanceName, ParticipantNodeConfig] + def remoteSequencers: Map[InstanceName, RemoteSequencerConfig] + def remoteMediators: Map[InstanceName, RemoteMediatorConfig] + def remoteParticipants: Map[InstanceName, RemoteParticipantConfig] + def monitoring: MonitoringConfig + def parameters: CantonParameters + def features: CantonFeatures /** Names of local nodes in order: sequencers, mediators, participants. Order within each group * may be different between runs, but the list may serve as a single reference order in case we @@ -466,16 +456,6 @@ final case class CantonConfig( n.unwrap -> c } - /** dump config to string (without sensitive data) */ - /** renders the config as a string (used for dumping config for diagnostic purposes) */ - def dumpString: String = CantonConfig.makeConfidentialString(this) - - /** run a validation on the current config and return possible warning messages */ - private def validate( - ensurePortsSet: Boolean - ): Validated[NonEmpty[Seq[String]], Unit] = - ConfigValidations.validate(this, ensurePortsSet = ensurePortsSet) - private lazy val participantNodeParameters_ : Map[InstanceName, ParticipantNodeParameters] = participants.fmap { participantConfig => val participantParameters = participantConfig.parameters @@ -516,7 +496,7 @@ final case class CantonConfig( commitmentUseDbSnapshotForParticipantLookup = participantParameters.commitmentUseDbSnapshotForParticipantLookup, autoSyncProtocolFeatureFlags = participantParameters.autoSyncProtocolFeatureFlags, - alphaMultiSynchronizerSupport = participantParameters.alphaMultiSynchronizerSupport, + enableAllLedgerApiReassignments = participantParameters.enableAllLedgerApiReassignments, commitAfterFailedActivenessCheck = participantParameters.commitAfterFailedActivenessCheck, validateLegacyContractsV11 = participantParameters.validateLegacyContractsV11, ) @@ -551,11 +531,14 @@ final case class CantonConfig( .map(DisasterRecoverySequencingTimeUpperBound(_)), delayRequestsBeforeLsuTrafficInit = sequencerNodeConfig.parameters.delayRequestsBeforeLsuTrafficInit, + enableRejectDeliveredAggregationsOnPv35 = + sequencerNodeConfig.parameters.enableRejectDeliveredAggregationsOnPv35, disableSubmissionChecksForTesting = sequencerNodeConfig.parameters.disableSubmissionChecksForTesting, disableReleaseVersionHandshakeCheck = sequencerNodeConfig.parameters.disableReleaseVersionHandshakeCheck, lsuConfig = sequencerNodeConfig.parameters.lsu, + enablePrevalidation = sequencerNodeConfig.parameters.enablePrevalidation, ) } @@ -571,6 +554,7 @@ final case class CantonConfig( MediatorNodeParameters( general = CantonNodeParameterConverter.general(this, mediatorNodeConfig), protocol = CantonNodeParameterConverter.protocol(this, mediatorNodeConfig.parameters), + delayedVerdictSender = mediatorNodeConfig.parameters.delayedVerdictSender, ) } @@ -592,6 +576,75 @@ final case class CantonConfig( ), ) + def dumpString: String + + def mergeDynamicChanges(newConfig: Self): Self + + protected def nodePortsDescription( + nodeName: InstanceName, + portDescriptions: Seq[String], + ): String = + s"$nodeName:${portDescriptions.mkString(",")}" + + protected def portDescriptionFromConfig[C]( + config: C + )(apiNamesAndExtractors: Seq[(String, C => ServerConfig)]): Seq[String] = { + def server(name: String, config: ServerConfig): Option[String] = + Option(config).map(c => s"$name=${c.port}") + Option(config) + .map(config => + apiNamesAndExtractors.map { case (name, extractor) => + server(name, extractor(config)) + } + ) + .getOrElse(Seq.empty) + .flatMap(_.toList) + } +} + +/** Root configuration parameters for a single Canton process. + * + * @param participants + * All locally running participants that this Canton process can connect and operate on. + * @param remoteParticipants + * All remotely running participants to which the console can connect and operate on. + * @param sequencers + * All locally running sequencers that this Canton process can connect and operate on. + * @param remoteSequencers + * All remotely running sequencers that this Canton process can connect and operate on. + * @param mediators + * All locally running mediators that this Canton process can connect and operate on. + * @param remoteMediators + * All remotely running mediators that this Canton process can connect and operate on. + * @param monitoring + * determines how this Canton process can be monitored + * @param parameters + * per-environment parameters to control enabled features and set testing parameters + * @param features + * control which features are enabled + */ +final case class CantonConfig( + sequencers: Map[InstanceName, SequencerNodeConfig] = Map.empty, + mediators: Map[InstanceName, MediatorNodeConfig] = Map.empty, + participants: Map[InstanceName, ParticipantNodeConfig] = Map.empty, + remoteSequencers: Map[InstanceName, RemoteSequencerConfig] = Map.empty, + remoteMediators: Map[InstanceName, RemoteMediatorConfig] = Map.empty, + remoteParticipants: Map[InstanceName, RemoteParticipantConfig] = Map.empty, + monitoring: MonitoringConfig = MonitoringConfig(), + parameters: CantonParameters = CantonParameters(), + features: CantonFeatures = CantonFeatures(), +) extends SharedCantonConfig[CantonConfig] { + + /** dump config to string (without sensitive data) */ + /** renders the config as a string (used for dumping config for diagnostic purposes) */ + def dumpString: String = CantonConfig.makeConfidentialString(this) + + /** run a validation on the current config and return possible warning messages */ + private def validate( + ensurePortsSet: Boolean + ): Validated[NonEmpty[Seq[String]], Unit] = + ConfigValidations.validate(this, ensurePortsSet = ensurePortsSet) + /** Produces a message in the structure * "da:admin-api=1,public-api=2;participant1:admin-api=3,ledger-api=4". Helpful for diagnosing * port already bound issues during tests. Allows any config value to be null (can happen with @@ -620,27 +673,6 @@ final case class CantonConfig( .mkString(";") } - protected def nodePortsDescription( - nodeName: InstanceName, - portDescriptions: Seq[String], - ): String = - s"$nodeName:${portDescriptions.mkString(",")}" - - protected def portDescriptionFromConfig[C]( - config: C - )(apiNamesAndExtractors: Seq[(String, C => ServerConfig)]): Seq[String] = { - def server(name: String, config: ServerConfig): Option[String] = - Option(config).map(c => s"$name=${c.port}") - Option(config) - .map(config => - apiNamesAndExtractors.map { case (name, extractor) => - server(name, extractor(config)) - } - ) - .getOrElse(Seq.empty) - .flatMap(_.toList) - } - /** reduces the configuration into a single node configuration (used for external testing) */ def asSingleNode(instanceName: InstanceName): CantonConfig = // based on the assumption that clashing instance names would clash in the console @@ -668,7 +700,7 @@ final case class CantonConfig( .modify(mapWithDefaults) } - def mergeDynamicChanges(newConfig: CantonConfig): CantonConfig = { + override def mergeDynamicChanges(newConfig: CantonConfig): CantonConfig = { def merge[T](cur: Map[InstanceName, T], newConfig: Map[InstanceName, T], merger: (T, T) => T) = cur.map { case (name, config) => (name, newConfig.get(name).map(merger(config, _)).getOrElse(config)) @@ -692,7 +724,7 @@ final case class CantonConfig( private[canton] object CantonNodeParameterConverter { - def general(parent: CantonConfig, node: LocalNodeConfig): CantonNodeParameters.General = + def general(parent: SharedCantonConfig[?], node: LocalNodeConfig): CantonNodeParameters.General = CantonNodeParameters.General.Impl( tracing = parent.monitoring.tracing, delayLoggingThreshold = parent.monitoring.logging.delayLoggingThreshold.toInternal, @@ -713,7 +745,10 @@ private[canton] object CantonNodeParameterConverter { sanitizePublicErrorMessages = parent.monitoring.sanitizePublicErrorMessages, ) - def protocol(parent: CantonConfig, config: ProtocolConfig): CantonNodeParameters.Protocol = + def protocol( + parent: SharedCantonConfig[?], + config: ProtocolConfig, + ): CantonNodeParameters.Protocol = CantonNodeParameters.Protocol.Impl( alphaVersionSupport = parent.parameters.alphaVersionSupport || config.alphaVersionSupport, betaVersionSupport = parent.parameters.betaVersionSupport || config.betaVersionSupport, @@ -1203,6 +1238,9 @@ object CantonConfig { lazy implicit val bftBlockOrdererLeaderSelectionPolicyConfigReader : ConfigReader[BlacklistLeaderSelectionPolicyConfig] = deriveReader[BlacklistLeaderSelectionPolicyConfig] + lazy implicit val bftBlockOrdererSequencerCoreSubscriptionConfigReader + : ConfigReader[SequencerCoreSubscriptionConfig] = + deriveReader[SequencerCoreSubscriptionConfig] lazy implicit val bftBlockOrdererConfigReader: ConfigReader[BftBlockOrdererConfig] = deriveReader[BftBlockOrdererConfig] lazy implicit val sequencerConfigBftSequencerReader @@ -1292,16 +1330,35 @@ object CantonConfig { lazy implicit final val remoteSequencerConfigReader: ConfigReader[RemoteSequencerConfig] = deriveReader[RemoteSequencerConfig] + lazy implicit final val mediatorNodeParameterConfigReader - : ConfigReader[MediatorNodeParameterConfig] = + : ConfigReader[MediatorNodeParameterConfig] = { + implicit val verdictSenderReaderConfig: ConfigReader[DelayedVerdictSenderConfig] = { + import NonNegativeNumeric.* + deriveReader[DelayedVerdictSenderConfig] + } deriveReader[MediatorNodeParameterConfig] + } lazy implicit final val mediatorConfigReader: ConfigReader[MediatorConfig] = { implicit val mediatorPruningConfigReader: ConfigReader[MediatorPruningConfig] = deriveReader[MediatorPruningConfig] implicit val deduplicationStoreConfigReader: ConfigReader[DeduplicationStoreConfig] = deriveReader[DeduplicationStoreConfig] - deriveReader[MediatorConfig] + + implicit val deprecatedFields: DeprecatedFieldsFor[MediatorConfig] = + new DeprecatedFieldsFor[MediatorConfig] { + + override def deprecatePath: List[DeprecatedConfigPath[?]] = + List( + DeprecatedConfigPath[Boolean]( + "asynchronous-processing", + since = "3.5.1", + ) + ) + } + + deriveReader[MediatorConfig].applyDeprecations } lazy implicit final val remoteMediatorConfigReader: ConfigReader[RemoteMediatorConfig] = deriveReader[RemoteMediatorConfig] @@ -1469,6 +1526,11 @@ object CantonConfig { since = "3.5.0", to = Seq("alpha-online-party-replication-support"), ), + DeprecatedConfigUtils.MovedConfigPath( + "alpha-multi-synchronizer-support", + since = "3.5.4", + to = Seq("enable-all-ledger-api-reassignments"), + ), ) override def deprecatePath: List[DeprecatedConfigPath[?]] = List( @@ -1525,7 +1587,20 @@ object CantonConfig { implicit val reassignmentsReader: ConfigReader[ReassignmentsConfig] = deriveReader[ReassignmentsConfig] implicit val purgeReader: ConfigReader[PurgeConfig] = deriveReader[PurgeConfig] - implicit val lsuReader: ConfigReader[LsuConfig] = deriveReader[LsuConfig] + implicit val lsuHandshakeReader: ConfigReader[LsuHandshake] = deriveReader[LsuHandshake] + + implicit val deprecatedFieldsLsuConfig: DeprecatedFieldsFor[LsuConfig] = + new DeprecatedFieldsFor[LsuConfig] { + override def movedFields: List[DeprecatedConfigUtils.MovedConfigPath] = List( + DeprecatedConfigUtils.MovedConfigPath( + "handshake-retry", + since = "3.5.1", + to = Seq("handshake.retry"), + ) + ) + } + + implicit val lsuReader: ConfigReader[LsuConfig] = deriveReader[LsuConfig].applyDeprecations deriveReader[ParticipantNodeParameterConfig].applyDeprecations } lazy implicit final val timeTrackerConfigReader: ConfigReader[SynchronizerTimeTrackerConfig] = { @@ -1615,6 +1690,21 @@ object CantonConfig { import DeclarativeSequencerConfig.Readers.* deriveReader[SequencerNodeConfig] } + + lazy implicit val trafficEnforcementProjectionConfigReader + : ConfigReader[TrafficEnforcementServerConfig.ProjectionConfig] = + deriveReader[TrafficEnforcementServerConfig.ProjectionConfig] + + lazy implicit val trafficEnforcementConfigInternalReader + : ConfigReader[TrafficEnforcementServerConfig.Internal] = + deriveReader[TrafficEnforcementServerConfig.Internal] + + lazy implicit val TrafficEnforcementServerConfigReader + : ConfigReader[TrafficEnforcementServerConfig] = + deriveReader[TrafficEnforcementServerConfig] + + lazy implicit val trafficEnforcementConfigReader: ConfigReader[TrafficEnforcementConfig] = + deriveReader[TrafficEnforcementConfig] } private implicit def cantonConfigReader(implicit @@ -2023,6 +2113,9 @@ object CantonConfig { lazy implicit val bftBlockOrdererLeaderSelectionPolicyConfigWriter : ConfigWriter[BlacklistLeaderSelectionPolicyConfig] = deriveWriter[BlacklistLeaderSelectionPolicyConfig] + lazy implicit val bftBlockOrdererSequencerCoreSubscriptionConfigWriter + : ConfigWriter[SequencerCoreSubscriptionConfig] = + deriveWriter[SequencerCoreSubscriptionConfig] lazy implicit val bftBlockOrdererConfigWriter: ConfigWriter[BftBlockOrdererConfig] = deriveWriter[BftBlockOrdererConfig] @@ -2103,8 +2196,11 @@ object CantonConfig { deriveWriter[MediatorConfig] } lazy implicit final val mediatorNodeParameterConfigWriter - : ConfigWriter[MediatorNodeParameterConfig] = + : ConfigWriter[MediatorNodeParameterConfig] = { + implicit val verdictSenderConfigWriter: ConfigWriter[DelayedVerdictSenderConfig] = + deriveWriter[DelayedVerdictSenderConfig] deriveWriter[MediatorNodeParameterConfig] + } lazy implicit final val remoteMediatorConfigWriter: ConfigWriter[RemoteMediatorConfig] = deriveWriter[RemoteMediatorConfig] @@ -2241,6 +2337,7 @@ object CantonConfig { implicit val reassignmentsConfigWriter: ConfigWriter[ReassignmentsConfig] = deriveWriter[ReassignmentsConfig] implicit val purgeWriter: ConfigWriter[PurgeConfig] = deriveWriter[PurgeConfig] + implicit val lsuHandshakeWriter: ConfigWriter[LsuHandshake] = deriveWriter[LsuHandshake] implicit val lsuWriter: ConfigWriter[LsuConfig] = deriveWriter[LsuConfig] deriveWriter[ParticipantNodeParameterConfig] } @@ -2315,6 +2412,21 @@ object CantonConfig { import DeclarativeSequencerConfig.Writers.* deriveWriter[SequencerNodeConfig] } + + lazy implicit val trafficEnforcementProjectionConfigWriter + : ConfigWriter[TrafficEnforcementServerConfig.ProjectionConfig] = + deriveWriter[TrafficEnforcementServerConfig.ProjectionConfig] + + lazy implicit val trafficEnforcementConfigInternalWriter + : ConfigWriter[TrafficEnforcementServerConfig.Internal] = + deriveWriter[TrafficEnforcementServerConfig.Internal] + + lazy implicit val trafficEnforcementServerConfigWriter + : ConfigWriter[TrafficEnforcementServerConfig] = + deriveWriter[TrafficEnforcementServerConfig] + + lazy implicit val trafficEnforcementConfigWriter: ConfigWriter[TrafficEnforcementConfig] = + deriveWriter[TrafficEnforcementConfig] } private def makeWriter(confidential: Boolean): ConfigWriter[CantonConfig] = { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala index f147cb1ef9..8474a3f44b 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/AdminCommandRunner.scala @@ -5,7 +5,7 @@ package com.digitalasset.canton.console import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand import com.digitalasset.canton.concurrent.Threading -import com.digitalasset.canton.config.{CantonConfig, NonNegativeDuration} +import com.digitalasset.canton.config.{NonNegativeDuration, SharedCantonConfig} import com.digitalasset.canton.console.CommandErrors.ConsoleTimeout import com.digitalasset.canton.crypto.Crypto import com.digitalasset.canton.environment.{CantonNode, CantonNodeBootstrap} @@ -103,7 +103,7 @@ trait FeatureFlagFilter extends NamedLogging { protected def consoleEnvironment: ConsoleEnvironment - protected def cantonConfig: CantonConfig = consoleEnvironment.environment.config + protected def cantonConfig: SharedCantonConfig[?] = consoleEnvironment.environment.config protected def check[T](flag: FeatureFlag)(command: => T): T = FeatureFlagFilter.checkEnabled(noTracingLogger)( diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala index 9389976626..7f15e21d3a 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/ConsoleEnvironment.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.console.commands.GlobalSecretKeyAdministration import com.digitalasset.canton.crypto.{Crypto, Fingerprint} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.environment.{CantonEnvironment, Environment} import com.digitalasset.canton.lifecycle.{FlagCloseable, LifeCycle} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.networking.grpc.CantonGrpcUtil @@ -62,15 +62,12 @@ object NodeReferences { NodeReferences[ParticipantReference, RemoteParticipantReference, LocalParticipantReference] } -/** The environment in which console commands are evaluated. - */ -@SuppressWarnings(Array("org.wartremover.warts.Any")) // required for `Binding[_]` usage -class ConsoleEnvironment( - val environment: Environment, - val consoleOutput: ConsoleOutput = StandardConsoleOutput, -) extends NamedLogging - with FlagCloseable - with NoTracing { +trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing { + type Config <: SharedCantonConfig[Config] + + val environment: Environment[Config] + + val consoleOutput: ConsoleOutput override protected val loggerFactory: NamedLoggerFactory = environment.loggerFactory @@ -522,6 +519,20 @@ class ConsoleEnvironment( def startAll(): Unit = runE(environment.startAll()) def stopAll(): Unit = runE(environment.stopAll()) +} + +/** The environment in which console commands are evaluated. + */ +@SuppressWarnings(Array("org.wartremover.warts.Any")) // required for `Binding[_]` usage +class CantonConsoleEnvironment( + override val environment: CantonEnvironment, + val consoleOutput: ConsoleOutput = StandardConsoleOutput, +) extends ConsoleEnvironment + with NamedLogging + with FlagCloseable + with NoTracing { + + override type Config = CantonConfig } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala index 0f8a608186..1d541eaf12 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/GrpcAdminCommandRunner.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.config.{ ClientConfig, ConsoleCommandTimeout, NonNegativeDuration, + SharedCantonConfig, } import com.digitalasset.canton.environment.Environment import com.digitalasset.canton.lifecycle.OnShutdownRunner @@ -178,7 +179,7 @@ class GrpcAdminCommandRunner( object GrpcAdminCommandRunner { def apply( - environment: Environment, + environment: Environment[? <: SharedCantonConfig[?]], apiName: String, ): GrpcAdminCommandRunner = new GrpcAdminCommandRunner( diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala index e6f3a3573b..3c7a6bcfba 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/HealthDumpGenerator.scala @@ -16,7 +16,7 @@ import com.digitalasset.canton.admin.api.client.data.{ DynamicSynchronizerParameters as ConsoleDynamicSynchronizerParameters, NodeStatus, } -import com.digitalasset.canton.config.LocalNodeConfig +import com.digitalasset.canton.config.{LocalNodeConfig, SharedCantonConfig} import com.digitalasset.canton.console.CommandErrors.CommandError import com.digitalasset.canton.console.HealthDumpGenerator.ParametersWithValidity import com.digitalasset.canton.environment.Environment @@ -38,7 +38,7 @@ import java.time.Instant * the core of the implementation of the HealthDump gRPC endpoint. */ class HealthDumpGenerator( - val environment: Environment, + val environment: Environment[? <: SharedCantonConfig[?]], val grpcAdminCommandRunner: GrpcAdminCommandRunner, override val loggerFactory: NamedLoggerFactory, ) extends NamedLogging { diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala index 7ad0e1635b..390b3c85bd 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ExternalPartiesTestingAdministration.scala @@ -9,7 +9,10 @@ import cats.syntax.either.* import cats.syntax.parallel.* import com.daml.nonempty.NonEmpty import com.daml.nonempty.catsinstances.* -import com.digitalasset.canton.admin.api.client.data.ListPartiesResult +import com.digitalasset.canton.admin.api.client.data.{ + KnownPhysicalSynchronizerId, + ListPartiesResult, +} import com.digitalasset.canton.config.ConsoleCommandTimeout import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.{ @@ -36,7 +39,6 @@ import com.digitalasset.canton.topology.transaction.{ } import com.digitalasset.canton.topology.{ ExternalParty, - KnownPhysicalSynchronizerId, Namespace, ParticipantId, PartyId, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala index c7aa9cdac5..8bd986d9ad 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/LedgerApiAdministration.scala @@ -102,6 +102,7 @@ import com.digitalasset.canton.networking.grpc.{ import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil import com.digitalasset.canton.platform.apiserver.execution.CommandStatus import com.digitalasset.canton.protocol.LfContractId +import com.digitalasset.canton.tea.v1.{GetAccountResponse, UpdateAccountResponse} import com.digitalasset.canton.topology.transaction.TopologyTransaction.GenericTopologyTransaction import com.digitalasset.canton.topology.{ ExternalParty, @@ -836,6 +837,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper userId: String = userId, deduplicationPeriod: Option[DeduplicationPeriod] = None, minLedgerTimeAbs: Option[Instant] = None, + optTimeout: Option[config.NonNegativeDuration] = Some(timeouts.ledgerCommand), ): ExecuteAndWaitResponseProto = consoleEnvironment.run { ledgerApiCommand( @@ -847,6 +849,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper deduplicationPeriod = deduplicationPeriod, minLedgerTimeAbs = minLedgerTimeAbs, hashingSchemeVersion = hashingSchemeVersion, + optTimeout = optTimeout, ) ) } @@ -873,6 +876,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper minLedgerTimeAbs: Option[Instant] = None, includeCreatedEventBlob: Boolean = false, customEventFormat: Option[EventFormat] = None, + optTimeout: Option[config.NonNegativeDuration] = Some(timeouts.ledgerCommand), ): ApiTransaction = consoleEnvironment.run { ledgerApiCommand( @@ -887,6 +891,7 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper transactionShape = transactionShape, includeCreatedEventBlob = includeCreatedEventBlob, customEventFormat = customEventFormat, + optTimeout = optTimeout, ) ) }.getTransaction @@ -3573,6 +3578,33 @@ trait BaseLedgerApiAdministration extends NoTracing with StreamingCommandHelper .pipe(GetEventsByContractIdResponse.toJavaProto) } } + + @Help.Summary("Participant user traffic service") + @Help.Group("Traffic") + object traffic extends Helpful { + @Help.Summary("Get account details", FeatureFlag.Testing) + @Help.Description("Get the details for the specified account-id") + def get_account(accountId: String): GetAccountResponse = + consoleEnvironment.run { + ledgerApiCommand(LedgerApiCommands.Traffic.GetAccount(accountId)) + } + + @Help.Summary("Update details for the account-id", FeatureFlag.Testing) + @Help.Description( + """Update the account details (the balance) for the specified account-id. + |If unset, the balance will not be updated + |subsequent balance updates with the same deduplicationId will be ignored""" + ) + def update_account( + accountId: String, + balance: Option[Long], + deduplicationId: String = UUID.randomUUID().toString, + ): UpdateAccountResponse = consoleEnvironment.run { + ledgerApiCommand( + LedgerApiCommands.Traffic.UpdateAccount(accountId, balance, deduplicationId) + ) + } + } } /** @return diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala index b4ebf316df..dd79aff8ea 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/commands/ParticipantAdministration.scala @@ -75,7 +75,6 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.time.NonNegativeFiniteDuration import com.digitalasset.canton.topology.transaction.GrpcConnection import com.digitalasset.canton.topology.{ - ConfiguredPhysicalSynchronizerId, ParticipantId, PartyId, PhysicalSynchronizerId, @@ -248,7 +247,7 @@ private[console] object ParticipantCommands { config: SynchronizerConnectionConfig, performHandshake: Boolean, validation: SequencerConnectionValidation, - )(implicit consoleEnvironment: ConsoleEnvironment): ConsoleCommandResult[Unit] = + ): ConsoleCommandResult[Unit] = runner.adminCommand( ParticipantAdminCommands.SynchronizerConnectivity .RegisterSynchronizer( @@ -262,7 +261,7 @@ private[console] object ParticipantCommands { runner: AdminCommandRunner, config: SynchronizerConnectionConfig, validation: SequencerConnectionValidation, - )(implicit consoleEnvironment: ConsoleEnvironment): ConsoleCommandResult[Unit] = + ): ConsoleCommandResult[Unit] = runner.adminCommand( ParticipantAdminCommands.SynchronizerConnectivity .ConnectSynchronizer(config.toInternal, validation.toInternal) @@ -2566,14 +2565,20 @@ trait ParticipantAdministration extends FeatureFlagFilter { |currently connected to the synchronizer.""" ) def list_registered() - : Seq[(SynchronizerConnectionConfig, ConfiguredPhysicalSynchronizerId, Boolean)] = { - val result = consoleEnvironment.run { - adminCommand(ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers) + : Seq[(SynchronizerConnectionConfig, ConfiguredPhysicalSynchronizerId, Boolean)] = + consoleEnvironment.run { + adminCommand( + ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers + ) } - result.map { case (internalConfig, psid, connected) => - (SynchronizerConnectionConfig.fromInternal(internalConfig), psid, connected) + + @Help.Summary("List all the configured synchronizers of this participant") + def list_all_registered(): Seq[RegisteredSynchronizer] = + consoleEnvironment.run { + adminCommand( + ParticipantAdminCommands.SynchronizerConnectivity.ListAllRegisteredSynchronizers + ) } - } @Help.Summary("Returns true if a synchronizer is registered using the given alias") def is_registered(synchronizerAlias: SynchronizerAlias): Boolean = @@ -2624,7 +2629,7 @@ trait ParticipantAdministration extends FeatureFlagFilter { consoleEnvironment.runE { for { registeredSynchronizers <- adminCommand( - ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers + ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers ).toEither cfg <- registeredSynchronizers .collectFirst { @@ -2632,7 +2637,7 @@ trait ParticipantAdministration extends FeatureFlagFilter { if config.synchronizerAlias == synchronizerAlias && physicalSynchronizerId .forall(requestedPsid => knownPsid.toOption.contains(requestedPsid)) => - SynchronizerConnectionConfig.fromInternal(config) + config } .toRight( s"No active synchronizer $synchronizerAlias configured" + physicalSynchronizerId @@ -2734,7 +2739,7 @@ trait ParticipantAdministration extends FeatureFlagFilter { ): Unit = consoleEnvironment.run { adminCommand( ParticipantAdminCommands.SynchronizerConnectivity - .PerformManualLsu(currentPsid, successorPsid, upgradeTime, Right(config.toInternal)) + .PerformManualLsu(currentPsid, successorPsid, upgradeTime, Right(config)) ) } } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala index e05ddfd4b2..fc7fffe002 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeApiManager.scala @@ -100,6 +100,7 @@ object DeclarativeApiManager { traceContext: TraceContext, ): EitherT[Future, String, DeclarativeApiHandle[ParticipantNodeConfig]] = { val myLoggerFactory = loggerFactory.append("participant", name) + val api = new DeclarativeParticipantApi( name, initialConfig.ledgerApi.clientConfig, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala index 2f1268f1dc..61e8de4a4b 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/console/declarative/DeclarativeParticipantApi.scala @@ -189,8 +189,7 @@ class DeclarativeParticipantApi( val mapping = SynchronizerTrustCertificate( participantId, synchronizerId, - featureFlags = - (ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer +: oldFeatureFlags), + featureFlags = (ParticipantTopologyFeatureFlag.EnableMultiSynchronizer +: oldFeatureFlags), ) queryAdminApi( TopologyAdminCommands.Write.Propose( @@ -211,7 +210,7 @@ class DeclarativeParticipantApi( current <- fetchSynchronizerTrustCertificate(sid.synchronizerId) currentFeatureFlags = current.headOption.map(_.item.featureFlags).getOrElse(Seq.empty) shouldUpdate = !currentFeatureFlags.contains( - ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer + ParticipantTopologyFeatureFlag.EnableMultiSynchronizer ) done <- if (shouldUpdate) { @@ -711,9 +710,12 @@ class DeclarativeParticipantApi( ) def fetchConnections(): Either[String, Seq[(SynchronizerAlias, DeclarativeConnectionConfig)]] = - queryAdminApi(ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers) - .map(_.map { case (synchronizerConnectionConfig, _, _) => synchronizerConnectionConfig } - .map(toDeclarative)) + queryAdminApi( + ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers + ) + .map(_.map { case (synchronizerConnectionConfig, _, _) => + toDeclarative(synchronizerConnectionConfig.toInternal) + }) def getConnection( alias: SynchronizerAlias @@ -726,7 +728,7 @@ class DeclarativeParticipantApi( // cannot really remove connections for now, just disconnect and disable for { currentO <- queryAdminApi( - ParticipantAdminCommands.SynchronizerConnectivity.ListRegisteredSynchronizers + ParticipantAdminCommands.SynchronizerConnectivity.ListActiveRegisteredSynchronizers ).map(_.collectFirst { case (config, psidO, _) if config.synchronizerAlias == synchronizerAlias => (config, psidO) @@ -741,7 +743,7 @@ class DeclarativeParticipantApi( (currentConfig, psidO) = current _ <- queryAdminApi( ParticipantAdminCommands.SynchronizerConnectivity.ModifySynchronizerConnection( - config = currentConfig.copy(manualConnect = true), + config = currentConfig.copy(manualConnect = true).toInternal, synchronizerId = psidO.toOption, sequencerConnectionValidation = SequencerConnectionValidation.Disabled, ) diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala index 68c622d952..4151149939 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/CommunityEnvironmentFactory.scala @@ -9,14 +9,14 @@ import com.digitalasset.canton.participant.ParticipantNodeBootstrapFactoryImpl import com.digitalasset.canton.synchronizer.mediator.MediatorNodeBootstrapFactoryImpl import com.digitalasset.canton.synchronizer.sequencer.SequencerNodeBootstrapFactoryImpl -object CommunityEnvironmentFactory extends EnvironmentFactory { +object CommunityEnvironmentFactory extends CantonEnvironmentFactory { override def create( config: CantonConfig, loggerFactory: NamedLoggerFactory, testingConfigInternal: TestingConfigInternal, - ): Environment = - new Environment( + ): CantonEnvironment = + new CantonEnvironment( config, testingConfigInternal, ParticipantNodeBootstrapFactoryImpl, diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala index a91fe05940..2f495ea6de 100644 --- a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/Environment.scala @@ -12,6 +12,7 @@ import com.daml.metrics.api.{HistogramInventory, MetricsContext, MetricsInfoFilt import com.digitalasset.canton.concurrent.* import com.digitalasset.canton.config.* import com.digitalasset.canton.console.{ + CantonConsoleEnvironment, ConsoleEnvironment, ConsoleOutput, GrpcAdminCommandRunner, @@ -44,7 +45,7 @@ import com.digitalasset.canton.time.* import com.digitalasset.canton.tracing.TraceContext.withNewTraceContext import com.digitalasset.canton.tracing.{NoTracing, TraceContext, TracerProvider} import com.digitalasset.canton.util.FutureInstances.parallelFuture -import com.digitalasset.canton.util.{MonadUtil, Mutex, PekkoUtil, SingleUseCell} +import com.digitalasset.canton.util.{EitherTUtil, MonadUtil, Mutex, PekkoUtil, SingleUseCell} import com.google.common.annotations.VisibleForTesting import io.circe.Encoder import io.circe.generic.semiauto.deriveEncoder @@ -60,8 +61,8 @@ import scala.util.control.NonFatal /** Holds all significant resources held by this process. */ -class Environment( - initialConfig: CantonConfig, +abstract class Environment[Config <: SharedCantonConfig[Config]]( + initialConfig: Config, val testingConfig: TestingConfigInternal, participantNodeFactory: ParticipantNodeBootstrapFactory, sequencerNodeFactory: SequencerNodeBootstrapFactory, @@ -71,15 +72,21 @@ class Environment( with AutoCloseable with NoTracing { + type Console <: ConsoleEnvironment + + protected def _createConsole( + consoleOutput: ConsoleOutput = StandardConsoleOutput + ): Console + implicit val scheduler: ScheduledExecutorService = Threading.singleThreadScheduledExecutor( loggerFactory.threadName + "-env-sched", noTracingLogger, ) - def config: CantonConfig = currentConfig.get() + def config: Config = currentConfig.get() def pokeOrUpdateConfig( - newConfig: Option[Either[String, CantonConfig]] + newConfig: Option[Either[String, Config]] )(implicit traceContext: TraceContext): Unit = { def pokeDeclarativeApis(configState: Either[Unit, Boolean]): Unit = Seq(sequencers, mediators, participants).foreach { group => @@ -107,7 +114,7 @@ class Environment( } } - private val currentConfig = new AtomicReference[CantonConfig](initialConfig) + private val currentConfig = new AtomicReference[Config](initialConfig) private val histogramInventory = new HistogramInventory() private val histograms = new CantonHistograms()(histogramInventory) private val baseFilter = new MetricsInfoFilter( @@ -151,8 +158,7 @@ class Environment( def createConsole( consoleOutput: ConsoleOutput = StandardConsoleOutput ): ConsoleEnvironment = { - val console = - new ConsoleEnvironment(this, consoleOutput) + val console = _createConsole(consoleOutput) healthDumpGenerator .putIfAbsent(createHealthDumpGenerator(console.grpcAdminCommandRunner)) .discard @@ -366,6 +372,7 @@ class Environment( runner writePortsFile() } // write ports after the runner has completed + // log results startup .leftMap(error => logger.error(s"Failed to start ${error.name}: ${error.message}")) @@ -418,12 +425,19 @@ class Environment( } EitherT.rightT(()) case Some(node) => - node - .reconnectSynchronizersIgnoreFailures(isTriggeredManually = false) - .leftMap(err => StartFailed(instance.name.unwrap, err.toString)) - .onShutdown(Left(StartFailed(instance.name.unwrap, "aborted due to shutdown"))) - + if (node.config.parameters.connectToSynchronizersOnStartup) + node + .reconnectSynchronizersIgnoreFailures(isTriggeredManually = false) + .leftMap(err => StartFailed(instance.name.unwrap, err.toString)) + .onShutdown(Left(StartFailed(instance.name.unwrap, "aborted due to shutdown"))) + else { + logger.info( + s"Not reconnecting $node to synchronizers because reconnect on startup is disabled" + ) + EitherTUtil.unit + } } + config.parameters.timeouts.processing.unbounded.await("reconnect-participants")( MonadUtil .parTraverseWithLimit_(config.parameters.getStartupParallelism(numThreads))( @@ -625,10 +639,31 @@ object Environment { } -trait EnvironmentFactory { +trait EnvironmentFactory[C <: SharedCantonConfig[C], E <: Environment[C]] { def create( - config: CantonConfig, + config: C, loggerFactory: NamedLoggerFactory, testingConfigInternal: TestingConfigInternal = TestingConfigInternal(), - ): Environment + ): E +} + +class CantonEnvironment( + initialConfig: CantonConfig, + override val testingConfig: TestingConfigInternal, + participantNodeFactory: ParticipantNodeBootstrapFactory, + sequencerNodeFactory: SequencerNodeBootstrapFactory, + mediatorNodeFactory: MediatorNodeBootstrapFactory, + override val loggerFactory: NamedLoggerFactory, +) extends Environment[CantonConfig]( + initialConfig, + testingConfig, + participantNodeFactory, + sequencerNodeFactory, + mediatorNodeFactory, + loggerFactory, + ) { + + override type Console = CantonConsoleEnvironment + override def _createConsole(output: ConsoleOutput) = + new CantonConsoleEnvironment(this, output) } diff --git a/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala new file mode 100644 index 0000000000..0c3354b80d --- /dev/null +++ b/canton/community/app-base/src/main/scala/com/digitalasset/canton/environment/package.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton + +import config.CantonConfig + +package object environment { + type CantonEnvironmentFactory = EnvironmentFactory[CantonConfig, CantonEnvironment] +} diff --git a/canton/community/app/src/main/resources/sandbox/bootstrap.canton b/canton/community/app/src/main/resources/sandbox/bootstrap.canton index d3877aa525..cd89f751f8 100644 --- a/canton/community/app/src/main/resources/sandbox/bootstrap.canton +++ b/canton/community/app/src/main/resources/sandbox/bootstrap.canton @@ -15,7 +15,7 @@ val syncDefs = mediators.local .zipWithIndex .map { case ((m, s), i) => SyncDef(m, s, s"synchronizer-${i + 1}") } -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) syncDefs.foreach{ syncDef => val staticSynchronizerParameters = StaticSynchronizerParameters.defaults( diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala index 243b8a1421..14a1bea1d9 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonAppDriver.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.config.{ DefaultPorts, GCLoggingConfig, Generate, + SharedCantonConfig, } import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.environment.{Environment, EnvironmentFactory} @@ -27,7 +28,7 @@ import com.digitalasset.canton.tracing.{NoTracing, TraceContext} import com.digitalasset.canton.util.JarResourceUtils import com.digitalasset.canton.version.ReleaseVersion import com.sun.management.GarbageCollectionNotificationInfo -import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.config.ConfigFactory import org.slf4j.LoggerFactory import java.lang.management.ManagementFactory @@ -44,10 +45,12 @@ import scala.util.control.NonFatal */ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { - protected def environmentFactory: EnvironmentFactory + type Config <: SharedCantonConfig[Config] + type E <: Environment[Config] - protected def withManualStart(config: CantonConfig): CantonConfig = - config.copy(parameters = config.parameters.copy(manualStart = true)) + protected def environmentFactory: EnvironmentFactory[Config, E] + + protected def withManualStart(config: Config): Config protected def additionalVersions: Map[String, String] = Map.empty @@ -61,6 +64,9 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { Console.out.println(s"$name: $version") } + protected def logAppVersion(): Unit = + logger.info(s"Starting Canton version ${ReleaseVersion.current}") + // BE CAREFUL: Set the environment variables before you touch anything related to // logback as otherwise, the logback configuration will be read without these // properties being considered @@ -97,7 +103,8 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { case (None, _) => } - logger.info(s"Starting Canton version ${ReleaseVersion.current}") + logAppVersion() + if (cliOptions.logTruncate) { cliOptions.logFileAppender match { case LogFileAppender.Rolling => @@ -113,7 +120,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { // Canton does not die on a warning status. logbackStatusManager.remove(killingStatusListener) - private val environmentRef: AtomicReference[Option[Environment]] = new AtomicReference(None) + private val environmentRef: AtomicReference[Option[E]] = new AtomicReference(None) sys.runtime.addShutdownHook(new Thread(() => { try { logger.info("Shutting down...") @@ -167,7 +174,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { def loadConfigFromFiles( loggingString: String = "Starting up with resolved config" - )(implicit traceContext: TraceContext): Either[CantonConfigError, CantonConfig] = { + )(implicit traceContext: TraceContext): Either[CantonConfigError, Config] = { val mergedUserConfigsE = NonEmpty.from(configFiles) match { case None if cliOptions.configMap.isEmpty => Left(ConfigErrors.NoConfigFiles.Error()) @@ -212,7 +219,7 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { sys.exit(1) } - private def writeConfigToTmpFile(mergedUserConfigs: Config) = { + private def writeConfigToTmpFile(mergedUserConfigs: com.typesafe.config.Config) = { val tmp = File.newTemporaryFile("canton-config-error-", ".conf") logger.error( s"An error occurred after parsing a config file that was obtained by merging multiple config " + @@ -289,11 +296,11 @@ abstract class CantonAppDriver extends App with NamedLogging with NoTracing { } def loadConfig( - config: Config, + config: com.typesafe.config.Config, defaultPorts: Option[DefaultPorts], - ): Either[CantonConfigError, CantonConfig] + ): Either[CantonConfigError, Config] - private def startupConfigFileMonitoring(environment: Environment): Unit = + protected[this] def startupConfigFileMonitoring(environment: E): Unit = TraceContext.withNewTraceContext("config_file_monitoring") { implicit traceContext => def modificationTimestamp(): Long = Config.configFiles.map(_.lastModified()).foldLeft(0L) { case (acc, item) => diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala index b64820db89..5502de3dd1 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/CantonCommunityApp.scala @@ -5,17 +5,27 @@ package com.digitalasset.canton import com.digitalasset.canton.config.ConfigErrors.CantonConfigError import com.digitalasset.canton.config.{CantonConfig, DefaultPorts} -import com.digitalasset.canton.environment.{CommunityEnvironmentFactory, EnvironmentFactory} -import com.typesafe.config.Config +import com.digitalasset.canton.environment.{ + CantonEnvironment, + CommunityEnvironmentFactory, + EnvironmentFactory, +} object CantonCommunityApp extends CantonAppDriver { + override type Config = CantonConfig + + override type E = CantonEnvironment + override def loadConfig( - config: Config, + config: com.typesafe.config.Config, defaultPorts: Option[DefaultPorts], ): Either[CantonConfigError, CantonConfig] = CantonConfig.loadAndValidate(config, defaultPorts) - override protected def environmentFactory: EnvironmentFactory = + override protected def environmentFactory: EnvironmentFactory[Config, E] = CommunityEnvironmentFactory + + override def withManualStart(config: CantonConfig): CantonConfig = + config.copy(parameters = config.parameters.copy(manualStart = true)) } diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala index 7e58292b38..a364723967 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/Runner.scala @@ -19,10 +19,10 @@ import scala.util.control.NonFatal */ trait Runner extends NamedLogging { - def run(environment: Environment): Unit + def run(environment: Environment[?]): Unit // TODO(#24954): Convert to using declarative api, when it becomes available - def uploadDar(environment: Environment)(darPath: String): Unit = { + def uploadDar(environment: Environment[?])(darPath: String): Unit = { val consoleEnvironment = environment.createConsole() consoleEnvironment.participants.local .flatMap(_.underlying) @@ -48,7 +48,7 @@ class ServerRunner( ) extends Runner with NoTracing { - def run(environment: Environment): Unit = + def run(environment: Environment[?]): Unit = try { def start(): Unit = environment @@ -86,7 +86,7 @@ class ConsoleInteractiveRunner( override val loggerFactory: NamedLoggerFactory, dars: Seq[String] = Seq.empty, ) extends Runner { - def run(environment: Environment): Unit = { + def run(environment: Environment[?]): Unit = { val success = try { val consoleEnvironment = environment.createConsole() @@ -113,7 +113,7 @@ class ConsoleScriptRunner( private val Ok = 0 private val Error = 1 - override def run(environment: Environment): Unit = { + override def run(environment: Environment[?]): Unit = { val exitCode = ConsoleScriptRunner.run(environment, scriptPath, logger) match { case Right(_unit) => @@ -187,14 +187,14 @@ object ConsoleScriptRunner extends NoTracing { new ConsoleScriptRunner(CantonScriptFromFile(scriptPath), loggerFactory) def run( - environment: Environment, + environment: Environment[?], scriptPath: File, logger: TracedLogger, ): Either[HeadlessConsole.HeadlessConsoleError, Unit] = run(environment, CantonScriptFromFile(scriptPath), logger) def run( - environment: Environment, + environment: Environment[?], cantonScript: CantonScript, logger: TracedLogger, ): Either[HeadlessConsole.HeadlessConsoleError, Unit] = { diff --git a/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala b/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala index 059ff67f0c..e25ef9f34e 100644 --- a/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala +++ b/canton/community/app/src/main/scala/com/digitalasset/canton/config/Generate.scala @@ -21,7 +21,7 @@ object Generate { ) } - def process(command: Command.Generate.Target, config: CantonConfig): Unit = + def process(command: Command.Generate.Target, config: SharedCantonConfig[?]): Unit = command match { case Command.Generate.RemoteConfig => val writers = new CantonConfig.ConfigWriters(confidential = false) diff --git a/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton b/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton index 0ca0a797fd..fa3fd4d790 100644 --- a/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton +++ b/canton/community/app/src/pack/examples/05-composability/composability-auto-reassignment.canton @@ -32,7 +32,7 @@ paintSynchronizerOwner.topology.synchronizer_parameters .propose_update(paintId, _.update(assignmentExclusivityTimeout = 2 seconds)) // enable multi-synchronizer topology feature flag for all participants for both synchronizers -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) participant1.topology.synchronizer_trust_certificates.propose(participant1, iouId, featureFlags = featureFlag) participant2.topology.synchronizer_trust_certificates.propose(participant2, iouId, featureFlags = featureFlag) participant3.topology.synchronizer_trust_certificates.propose(participant3, iouId, featureFlags = featureFlag) diff --git a/canton/community/app/src/pack/examples/05-composability/composability1.canton b/canton/community/app/src/pack/examples/05-composability/composability1.canton index e2b98b63f5..f6b1ebe15c 100644 --- a/canton/community/app/src/pack/examples/05-composability/composability1.canton +++ b/canton/community/app/src/pack/examples/05-composability/composability1.canton @@ -41,7 +41,7 @@ participant2.synchronizers.connect_local(paint_sequencer, alias = paintAlias) participant3.synchronizers.connect_local(paint_sequencer, alias = paintAlias) // enable multi-synchronizer topology feature flag for all participants for both synchronizers -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) participant1.topology.synchronizer_trust_certificates.propose(participant1, iouId, featureFlags = featureFlag) participant2.topology.synchronizer_trust_certificates.propose(participant2, iouId, featureFlags = featureFlag) participant3.topology.synchronizer_trust_certificates.propose(participant3, iouId, featureFlags = featureFlag) diff --git a/canton/community/app/src/pack/examples/05-composability/composability2.canton b/canton/community/app/src/pack/examples/05-composability/composability2.canton index b9e7bde715..8359962b68 100644 --- a/canton/community/app/src/pack/examples/05-composability/composability2.canton +++ b/canton/community/app/src/pack/examples/05-composability/composability2.canton @@ -33,7 +33,7 @@ paintSynchronizerOwner.topology.synchronizer_parameters .propose_update(paintId, _.update(assignmentExclusivityTimeout = 2 seconds)) // enable multi-synchronizer topology feature flag for all participants for both synchronizers -val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableAlphaMultiSynchronizer) +val featureFlag = Seq(SynchronizerTrustCertificate.ParticipantTopologyFeatureFlag.EnableMultiSynchronizer) participant1.topology.synchronizer_trust_certificates.propose(participant1, iouId, featureFlags = featureFlag) participant2.topology.synchronizer_trust_certificates.propose(participant2, iouId, featureFlags = featureFlag) participant3.topology.synchronizer_trust_certificates.propose(participant3, iouId, featureFlags = featureFlag) diff --git a/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt b/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt index a4a5bbbc4a..2b959f353e 100644 --- a/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt +++ b/canton/community/app/src/pack/examples/08-interactive-submission/requirements.txt @@ -1,3 +1,3 @@ -cryptography==44.0.1 +cryptography>=46.0.7 grpcio-tools==1.70.0 protobuf==5.29.3 diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala index c60f39e833..138f823390 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/config/CantonConfigTest.scala @@ -605,7 +605,7 @@ class CantonConfigTest extends AnyWordSpec with BaseTest { participant.httpLedgerApi.requestTimeout.toMinutes shouldBe 105 // verify that `crypto.sessionSigningKeys` is configured with the expected values - participant.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.default + participant.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.enabled } // In this test case, both deprecated and new fields are set with opposite values, we make sure the new fields @@ -696,5 +696,71 @@ class CantonConfigTest extends AnyWordSpec with BaseTest { ) } } - + "AuthServiceConfig parsing" should { + "correctly parse max-token-life for all provider types" in { + import com.digitalasset.canton.config.AuthServiceConfig.* + import scala.concurrent.duration.* + + val authConfigStr = + """ + |canton.participants.participant1.ledger-api.auth-services = [ + | { + | type = "unsafe-jwt-hmac-256" + | secret = "super-secret-test-key-here" + | max-token-life = "10m" + | }, + | { + | type = "jwt-rs-256-crt" + | certificate = "path/to/rsa-cert.crt" + | max-token-life = "20m" + | }, + | { + | type = "jwt-jwks" + | url = "https://example.com/.well-known/jwks.json" + | max-token-life = "30m" + | }, + | { + | type = "jwt-es-256-crt" + | certificate = "path/to/cert256.crt" + | max-token-life = "40m" + | }, + | { + | type = "jwt-es-512-crt" + | certificate = "path/to/cert512.crt" + | max-token-life = "50m" + | } + |] + |""".stripMargin + + File.usingTemporaryFile("auth-services-test", ".conf") { tempFile => + tempFile.writeText(authConfigStr) + + val parsedConfig = CantonConfig + .parseAndLoad( + Seq(simpleConf.toJava, tempFile.toJava), + Some(DefaultPorts.create()), + ) + .valueOrFail("Failed to parse config with auth services") + + val authServices = parsedConfig.participantsByString("participant1").ledgerApi.authServices + + authServices should have size 5 + + inside(authServices) { + case Seq( + unsafe: UnsafeJwtHmac256, + rs256: JwtRs256Crt, + jwks: JwtJwks, + es256: JwtEs256Crt, + es512: JwtEs512Crt, + ) => + unsafe.maxTokenLife.duration shouldBe 10.minutes + rs256.maxTokenLife.duration shouldBe 20.minutes + jwks.maxTokenLife.duration shouldBe 30.minutes + es256.maxTokenLife.duration shouldBe 40.minutes + es512.maxTokenLife.duration shouldBe 50.minutes + } + } + } + } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala index de61db16d2..4716b25e1a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/config/ConfigValidationsTest.scala @@ -689,7 +689,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { ), kms = Some(KmsConfig.Aws.defaultTestConfig), sessionSigningKeys = - SessionSigningKeysConfig.default.copy(signingAlgorithmSpec = signingAlgorithmSpec), + SessionSigningKeysConfig.enabled.copy(signingAlgorithmSpec = signingAlgorithmSpec), ) ) ), @@ -750,7 +750,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { ), kms = Some(KmsConfig.Aws.defaultTestConfig), sessionSigningKeys = - SessionSigningKeysConfig.default.copy(signingKeySpec = sessionSigningKeySpec), + SessionSigningKeysConfig.enabled.copy(signingKeySpec = sessionSigningKeySpec), ) ) ) @@ -767,16 +767,16 @@ class ConfigValidationsTest extends BaseTestWordSpec { crypto = CryptoConfig( provider = Kms, kms = Some(KmsConfig.Aws.defaultTestConfig), - sessionSigningKeys = SessionSigningKeysConfig.default, + sessionSigningKeys = SessionSigningKeysConfig.enabled, ) ) val config = CantonConfig(participants = Map(InstanceName.tryCreate("p1") -> participantNodeConfig)) - val defaultKeyValidityDuration = SessionSigningKeysConfig.default.keyValidityDuration - val defaultToleranceShiftDuration = SessionSigningKeysConfig.default.toleranceShiftDuration - val defaultCutOffDuration = SessionSigningKeysConfig.default.cutOffDuration - val defaultKeyEvictionPeriod = SessionSigningKeysConfig.default.keyEvictionPeriod + val defaultKeyValidityDuration = SessionSigningKeysConfig.enabled.keyValidityDuration + val defaultToleranceShiftDuration = SessionSigningKeysConfig.enabled.toleranceShiftDuration + val defaultCutOffDuration = SessionSigningKeysConfig.enabled.cutOffDuration + val defaultKeyEvictionPeriod = SessionSigningKeysConfig.enabled.keyEvictionPeriod def changeSessionSigningKeyParams( keyValidityDuration: Option[PositiveFiniteDuration], @@ -784,7 +784,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { cutOffDuration: Option[NonNegativeFiniteDuration] = None, keyEvictionPeriod: Option[PositiveFiniteDuration] = None, ) = { - val params = SessionSigningKeysConfig.default.copy( + val params = SessionSigningKeysConfig.enabled.copy( keyValidityDuration = keyValidityDuration.getOrElse(defaultKeyValidityDuration), toleranceShiftDuration = toleranceShiftDuration.getOrElse(defaultToleranceShiftDuration), cutOffDuration = cutOffDuration.getOrElse(defaultCutOffDuration), @@ -832,7 +832,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { )( s"participant p1: The selected session signing key tolerance shift duration " + s"of $invalidToleranceShiftDuration must be longer than the cut-off " + - s"(${SessionSigningKeysConfig.default.cutOffDuration})." + s"(${SessionSigningKeysConfig.enabled.cutOffDuration})." ) assertErrors( @@ -845,14 +845,14 @@ class ConfigValidationsTest extends BaseTestWordSpec { .focus(_.sequencerClient.defaultMaxSequencingTimeOffset) .replace( NonNegativeFiniteDuration.ofMinutes( - SessionSigningKeysConfig.default.keyValidityDuration.duration.toMinutes + SessionSigningKeysConfig.enabled.keyValidityDuration.duration.toMinutes ) ) ) ) )( s"participant p1: The selected session signing key validity parameters do not align with " + - s"the current default max sequencing time offset (${SessionSigningKeysConfig.default.keyValidityDuration}). " + + s"the current default max sequencing time offset (${SessionSigningKeysConfig.enabled.keyValidityDuration}). " + s"Parameters must be chosen so that " + s"`keyValidityDuration` - `cutOffDuration` > `defaultMaxSequencingTimeOffset`." ) @@ -866,13 +866,13 @@ class ConfigValidationsTest extends BaseTestWordSpec { )( s"participant p1: The selected session signing key eviction period " + s"of $invalidKeyEvictionPeriod must be longer than the key validity duration " + - s"(${SessionSigningKeysConfig.default.keyValidityDuration})." + s"(${SessionSigningKeysConfig.enabled.keyValidityDuration})." ) } "pass config validation with parameters out of bounds and checks disabled" in { - val invalidKeyValidityDuration = SessionSigningKeysConfig.default.keyEvictionPeriod + val invalidKeyValidityDuration = SessionSigningKeysConfig.enabled.keyEvictionPeriod val config = CantonConfig( parameters = CantonParameters(nonStandardConfig = true), participants = Map( @@ -881,7 +881,7 @@ class ConfigValidationsTest extends BaseTestWordSpec { crypto = CryptoConfig( provider = Kms, kms = Some(KmsConfig.Aws.defaultTestConfig), - sessionSigningKeys = SessionSigningKeysConfig.default.copy( + sessionSigningKeys = SessionSigningKeysConfig.enabled.copy( keyValidityDuration = invalidKeyValidityDuration, disableBoundChecks = true, ), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala index 8368305f4d..d7ccbf8b75 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleEnvironmentTest.scala @@ -7,7 +7,7 @@ import com.daml.metrics.OnDemandMetricsReader.NoOpOnDemandMetricsReader$ import com.digitalasset.canton.BaseTest import com.digitalasset.canton.config.CantonConfig import com.digitalasset.canton.console.CommandErrors.GenericCommandError -import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.environment.CantonEnvironment import com.digitalasset.canton.logging.{NamedEventCapturingLogger, SuppressingLogger} import com.digitalasset.canton.telemetry.ConfiguredOpenTelemetry import com.digitalasset.canton.tracing.TracerProvider @@ -36,9 +36,9 @@ class ConsoleEnvironmentTest extends AnyWordSpec with BaseTest { val testConsoleOutput: TestConsoleOutput = new TestConsoleOutput(capturingLoggerFactory) // Setup environment to inject capturing loggerFactory - val environment = mock[Environment] + val environment = mock[CantonEnvironment] when(environment.loggerFactory).thenReturn(capturingLoggerFactory) - when(environment.config).thenReturn(CantonConfig()) + doReturn(CantonConfig()).when(environment).config when(environment.tracerProvider).thenReturn(mock[TracerProvider]) when(environment.configuredOpenTelemetry).thenReturn( ConfiguredOpenTelemetry( @@ -49,7 +49,7 @@ class ConsoleEnvironmentTest extends AnyWordSpec with BaseTest { ) // The ConsoleEnvironment to be tested - val consoleEnvironment: ConsoleEnvironment = new ConsoleEnvironment( + val consoleEnvironment: CantonConsoleEnvironment = new CantonConsoleEnvironment( environment, consoleOutput = testConsoleOutput, ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala index 5bdeb9d3af..fde97a17a4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/console/ConsoleTest.scala @@ -71,7 +71,7 @@ final class ConsoleTest extends AnyWordSpec with BaseTest { ) private abstract class TestEnvironment(val config: CantonConfig = DefaultConfig) { - val environment: Environment = mock[Environment] + val environment: CantonEnvironment = mock[CantonEnvironment] val participants: ParticipantNodes[ParticipantNodeBootstrap, ParticipantNode] = mock[ParticipantNodes[ParticipantNodeBootstrap, ParticipantNode]] val sequencers: SequencerNodes = mock[SequencerNodes] @@ -82,7 +82,7 @@ final class ConsoleTest extends AnyWordSpec with BaseTest { val adminToken: String = "0" * 64 when(environment.tracerProvider).thenReturn(mock[TracerProvider]) - when(environment.config).thenReturn(config) + doReturn(config).when(environment).config when(environment.testingConfig).thenReturn( TestingConfigInternal(initializeGlobalOpenTelemetry = false) ) @@ -126,7 +126,7 @@ final class ConsoleTest extends AnyWordSpec with BaseTest { .thenReturn(GenericCommandError("Mocked error")) private val consoleEnvironment = - new ConsoleEnvironment( + new CantonConsoleEnvironment( environment, consoleOutput = testConsoleOutput, ) { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala index 176b380f02..f0dcbf94ca 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/CommunityEnvironmentFixture.scala @@ -123,7 +123,7 @@ trait CommunityEnvironmentFixture extends BaseTest with HasExecutionContext { th def mockParticipant: ParticipantNodeBootstrap = mockParticipantAndNode._1 - val environment = new Environment( + val environment = new CantonEnvironment( config, TestingConfigInternal(initializeGlobalOpenTelemetry = false, warnOnJwtScopeUsage = false), new ParticipantNodeBootstrapFactory { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala index db0093367d..8aeb13c3ef 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/environment/NodesTest.scala @@ -31,6 +31,7 @@ import com.digitalasset.canton.lifecycle.{LifeCycle, ShutdownFailedException} import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX import com.digitalasset.canton.metrics.{ CommonMockMetrics, + CryptoMetrics, DbStorageMetrics, DeclarativeApiMetrics, LedgerApiServerMetrics, @@ -126,6 +127,7 @@ class NodesTest extends FixtureAnyWordSpec with BaseTest with HasExecutionContex (LedgerApiServerMetrics.ForTesting.grpc, LedgerApiServerMetrics.ForTesting.requests), healthMetrics: HealthMetrics = LedgerApiServerMetrics.ForTesting.health, storageMetrics: DbStorageMetrics = CommonMockMetrics.dbStorage, + cryptoMetrics: CryptoMetrics = CommonMockMetrics.cryptoMetrics, ) extends BaseMetrics { override val declarativeApiMetrics: DeclarativeApiMetrics = diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala index 66ae4011c5..3b2f4f87f9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DownloadTopologyForInitIntegrationTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.integration.tests import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.config.{DbConfig, DefaultProcessingTimeouts} import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -46,9 +47,17 @@ abstract class DownloadTopologyForInitIntegrationTest Map( // A threshold of 2 ensures that the mediators connect to all sequencers. // TODO(#19911) Make this properly configurable - mediator1 -> (Seq(sequencer1, sequencer2), PositiveInt.two, NonNegativeInt.zero), + mediator1 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ), // Have this so that mediator2 gets registered in the topology state. - mediator2 -> (Seq(sequencer2), PositiveInt.one, NonNegativeInt.zero), + mediator2 -> MediatorSequencersConfiguration( + Seq(sequencer2), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ), ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala index 3f656853a4..63d0ac37ce 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/DumpIntegrationTest.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.integration.{ EnvironmentDefinition, SharedEnvironment, } +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.messages.AcsCommitment import com.digitalasset.canton.protocol.v30 import com.digitalasset.canton.sequencing.PossiblyIgnoredProtocolEvent @@ -64,6 +65,7 @@ sealed trait DumpIntegrationTest extends CommunityIntegrationTest with SharedEnv CryptoSchemes .fromConfig(config.crypto) .valueOrFail("fail to validate crypto schemes from the configuration file"), + CommonMockMetrics.cryptoMetrics, loggerFactory, ) .valueOr(err => throw new RuntimeException(s"Failed to create pure crypto api: $err")) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala index 219c069419..a7f59d023f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/MultipleMediatorsMultipleSynchronizersIntegrationTest.scala @@ -46,7 +46,7 @@ final class MultipleMediatorsMultipleSynchronizersIntegrationTest ).withManualStart .addConfigTransforms( ProgrammableSequencer.configOverride(this.getClass.toString, loggerFactory), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala index a9c399f1d5..6f634df783 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ProtobufCompatibilityTest.scala @@ -167,10 +167,41 @@ final class ProtobufCompatibilityReaderTest """com/digitalasset/canton/topology/admin/v30/topology_manager_read_service.proto:Previously present RPC "ListSequencerConnectionSuccessor" on service "TopologyManagerReadService" was deleted.""", """com/digitalasset/canton/topology/admin/v30/topology_manager_read_service.proto:Previously present RPC "ListSynchronizerUpgradeAnnouncement" on service "TopologyManagerReadService" was deleted.""", """com/digitalasset/canton/topology/admin/v30/topology_manager_read_service.proto:Previously present RPC "LogicalUpgradeState" on service "TopologyManagerReadService" was deleted.""", + + /// Backward compatibility + """com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.proto:Previously present field "3" with name "confirmation_response_factor" on message "SubmissionRequestAmplification" was deleted.""", + """com/digitalasset/canton/admin/sequencer/v30/sequencer_connection.proto:Previously present field "4" with name "confirmation_response_patience" on message "SubmissionRequestAmplification" was deleted.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Previously present field "2" with name "complete" on message "VerdictsResponse" was deleted.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Previously present oneof "payload" on message "VerdictsResponse" was deleted.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Field "1" with name "verdict" on message "VerdictsResponse" moved from inside to outside a oneof.""", + """com/digitalasset/canton/mediator/admin/v30/mediator_inspection_service.proto:Previously present field "4" with name "view_hash" on message "TransactionView" was deleted.""", + """com/digitalasset/canton/participant/protocol/v30/submission_tracking.proto:Previously present field "6" with name "paid_traffic_cost" on message "CompletionInfo" was deleted.""", + """com/digitalasset/canton/protocol/v30/synchronization.proto:Previously present field "7" with name "lsu_sequencing_test_message" on message "EnvelopeContent" was deleted.""", + """com/digitalasset/canton/protocol/v30/topology.proto:Previously present enum value "2" on enum "ParticipantFeatureFlag" was deleted.""", + """com/digitalasset/canton/protocol/v30/topology.proto:Field "17" with name "synchronizer_upgrade_announcement" on message "TopologyMapping" changed type from "com.digitalasset.canton.protocol.v30.LsuAnnouncement" to "com.digitalasset.canton.protocol.v30.SynchronizerUpgradeAnnouncement".""", + """com/digitalasset/canton/protocol/v30/topology.proto:Field "18" with name "sequencer_connection_successor" on message "TopologyMapping" changed type from "com.digitalasset.canton.protocol.v30.LsuSequencerConnectionSuccessor" to "com.digitalasset.canton.protocol.v30.SequencerConnectionSuccessor".""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "3" with name "dynamic_sequencing_parameters_payload" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "4" with name "dynamic_sequencing_parameters_payload31" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present oneof "dynamic_sequencing_parameters" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_authentication_service.proto:Previously present field "3" with name "client_version" on message "ChallengeRequest" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present field "3" with name "client_version" on message "HandshakeRequest" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved name "failure" on message "HandshakeResponse" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved range "[3]" on message "HandshakeResponse" is missing values: [3] were removed.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved name "failure" on message "VerifyActiveResponse" was deleted.""", + """com/digitalasset/canton/sequencer/api/v30/sequencer_connect_service.proto:Previously present reserved range "[2]" on message "VerifyActiveResponse" is missing values: [2] were removed.""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Previously present field "5" with name "ordering_start_instant" on message "OrderingRequest" was deleted.""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Field "3" with name "payload" on message "OrderingRequest" changed type from "string" to "bytes".""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Field "4" with name "ordering_start_instant" on message "OrderingRequest" changed cardinality from "optional with implicit presence" to "optional with explicit presence".""", + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Field "4" with name "ordering_start_instant" on message "OrderingRequest" changed type from "bytes" to "message".""", + """com/digitalasset/canton/synchronizer/v30/synchronizer.proto:Previously present field "3" with name "is_late_upgrade" on message "SynchronizerPredecessor" was deleted.""", + """com/digitalasset/canton/admin/health/v30/status_service.proto:Previously present field "3" with name "version" on message "NotInitialized" was deleted.""", + // Added DABFT leaders and blacklisted nodes to `get_ordering_topology` console admin function's output + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "5" with name "leader_sequencer_ids" on message "GetOrderingTopologyResponse" was deleted.""", + """com/digitalasset/canton/sequencer/admin/v30/sequencer_bft_administration_service.proto:Previously present field "6" with name "blacklisted_sequencer_ids" on message "GetOrderingTopologyResponse" was deleted.""", + // undefined epoch_number case is handled + """com/digitalasset/canton/synchronizer/sequencing/sequencer/bftordering/v30/bft_ordering_service.proto:Previously present field "2" with name "epoch_number" on message "BatchRequest" was deleted.""", ), (3, 5) -> Seq( - // Expose the release version in NotInitialized, is going to be added to 3.5.1-rc4+ - """com/digitalasset/canton/admin/health/v30/status_service.proto:Previously present field "3" with name "version" on message "NotInitialized" was deleted.""" ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala index d60de8e3ca..14f3f55a3d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedMediatorIntegrationTest.scala @@ -14,11 +14,11 @@ import com.digitalasset.canton.integration.bootstrap.{ } import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres, UseSharedStorage} import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransform, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, EnvironmentSetupPlugin, SharedEnvironment, TestConsoleEnvironment, @@ -31,7 +31,7 @@ import monocle.macros.syntax.lens.* import org.slf4j.event.Level trait ReplicatedMediatorTestSetup extends ReplicatedNodeHelper { - self: CommunityIntegrationTest & EnvironmentSetup => + self: CommunityIntegrationTest & CantonEnvironmentSetup => protected lazy val mediator1Name = "mediatorReplicated1" protected lazy val mediator2Name = "mediatorReplicated2" diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala index 2b165dd538..0ce14dfb6b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ReplicatedParticipantTest.scala @@ -29,10 +29,10 @@ import com.digitalasset.canton.integration.bootstrap.{ } import com.digitalasset.canton.integration.plugins.* import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, EnvironmentSetupPlugin, SharedEnvironment, TestConsoleEnvironment, @@ -113,7 +113,7 @@ trait ReplicatedNodeHelper { self: CommunityIntegrationTest => } trait ReplicatedParticipantTestSetup extends ReplicatedNodeHelper { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( storagePlugin: EnvironmentSetupPlugin, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala index d8580dc776..859d6c972a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SequencerIdsRetrieverIntegrationTest.scala @@ -13,7 +13,8 @@ import com.digitalasset.canton.admin.api.client.data.{ } import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.config.{ExponentialBackoffConfig, NonNegativeDuration} -import com.digitalasset.canton.console.{MediatorReference, SequencerReference} +import com.digitalasset.canton.console.MediatorReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -50,12 +51,18 @@ final class SequencerIdsRetrieverIntegrationTest Ensure each mediator is connected to a single sequencer. The goal is that if a sequencer is stopped (sequencer4 in this test) it does not impact */ - val mediatorToSequencers - : Map[MediatorReference, (Seq[SequencerReference], PositiveInt, NonNegativeInt)] = + val mediatorToSequencers: Map[MediatorReference, MediatorSequencersConfiguration] = sequencers .zip(mediators) .map { case (sequencer, mediator) => - (mediator, (Seq(sequencer), PositiveInt.one, NonNegativeInt.zero)) + ( + mediator, + MediatorSequencersConfiguration( + Seq(sequencer), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ), + ) } .toMap diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala index d26918eb3c..1fd18fe855 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SubmissionRequestAmplificationIntegrationTest.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.console.{ } import com.digitalasset.canton.integration.EnvironmentDefinition.S2M2 import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.plugins.{ UseBftSequencer, UsePostgres, @@ -79,8 +80,16 @@ abstract class SubmissionRequestAmplificationIntegrationTest Map( // A threshold of two ensures that the mediators connect to both sequencers. // TODO(#19911) Make this properly configurable - mediator1 -> (Seq(sequencer1, sequencer2), PositiveInt.two, NonNegativeInt.zero), - mediator2 -> (Seq(sequencer1, sequencer2), PositiveInt.two, NonNegativeInt.zero), + mediator1 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ), + mediator2 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ), ) ) ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala index 89064323f8..add3754a4b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerChangeIntegrationTest.scala @@ -110,7 +110,7 @@ abstract class SynchronizerChangeIntegrationTest(config: SynchronizerChangeInteg .updateTargetTimestampForwardTolerance( config.targetTimestampForwardTolerance.duration ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .addConfigTransforms(additionalConfigTransforms*) .withSetup(setUp) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala index 1e816b0938..a5f4b4f6cc 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerConnectivityIntegrationTest.scala @@ -144,7 +144,21 @@ sealed trait SynchronizerConnectivityIntegrationTest participant2.synchronizers.list_connected().map(_.synchronizerAlias) shouldBe Seq(daName) participant2.synchronizers.disconnect_all() participant2.synchronizers.list_connected() shouldBe empty + } + } + + "A participant" must { + "Be able to change the config" in { implicit env => + import env.* + + participant1.synchronizers.modify(daName, _.focus(_.priority).modify(_ + 1)) + participant1.synchronizers.modify( + daName, + _.focus(_.priority).modify(_ + 1), + physicalSynchronizerId = Some(daId), + ) + succeed } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala index aea5e11b14..a0dbf4a21a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/SynchronizerRouterIntegrationTestSetup.scala @@ -49,7 +49,7 @@ trait SynchronizerRouterIntegrationTestSetup EnvironmentDefinition .P4_S1M1_S1M1_S1M1() .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala index a2cec73877..a9c0076260 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/TickRequestIntegrationTest.scala @@ -89,7 +89,7 @@ sealed trait TickRequestIntegrationTest ConfigTransforms.useStaticTime, ConfigTransforms.updateSynchronizerTimeTrackerConfigs_(_ => synchronizerTimeTrackerConfig), ConfigTransforms.updateTargetTimestampForwardTolerance(Duration.ofHours(1)), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .addConfigTransforms( ConfigTransforms.setTopologyTransactionRegistrationTimeout( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala index 1704370b90..bd61cd163e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/ActiveContractsIntegrationTest.scala @@ -68,7 +68,7 @@ import scala.jdk.CollectionConverters.* import ActiveContractsIntegrationTestBase.* -abstract class ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport: Boolean = false) +abstract class ActiveContractsIntegrationTestBase(enableAllLedgerApiReassignments: Boolean = false) extends CommunityIntegrationTest with SharedEnvironment with AcsInspection @@ -96,9 +96,10 @@ abstract class ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport: // Ensure reassignments are not tripped up by some participants being a little behind. ConfigTransforms.updateTargetTimestampForwardTolerance(30.seconds), ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -263,7 +264,7 @@ abstract class ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport: val createdEvent = eventually() { val endOffset = participant1.ledger_api.state.end() - if (participant1.config.parameters.alphaMultiSynchronizerSupport) { + if (participant1.config.parameters.enableAllLedgerApiReassignments) { participant1.ledger_api.updates .reassignments( partyIds = Set(signatory), @@ -900,4 +901,4 @@ private object ActiveContractsIntegrationTestBase { final class ActiveContractsIntegrationTest extends ActiveContractsIntegrationTestBase final class ActiveContractsReassignmentIntegrationTest - extends ActiveContractsIntegrationTestBase(alphaMultiSynchronizerSupport = true) + extends ActiveContractsIntegrationTestBase(enableAllLedgerApiReassignments = true) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala index bf17f6cba9..0224256376 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentProcessorIntegrationTest.scala @@ -90,7 +90,7 @@ sealed trait AcsCommitmentProcessorIntegrationTest ConfigTransforms.useStaticTime, // this only sets/enables session signing keys when running with PV35 or higher ConfigTransforms.setSigningKeysIfPV35OrHigher( - SessionSigningKeysConfig.default.copy( + SessionSigningKeysConfig.enabled.copy( // we evict the session key cache right away to make sure we use a fresh session signing key for each request keyEvictionPeriod = config.PositiveFiniteDuration.ofMillis(1), // we must disable bound checks because `keyEvictionPeriod` is shorter than `keyValidityDuration` diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala index 9d08efae33..179c4250f1 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/AcsCommitmentToolingIntegrationTest.scala @@ -101,7 +101,7 @@ trait AcsCommitmentToolingIntegrationTest ConfigTransforms.useStaticTime, ConfigTransforms.updateMaxDeduplicationDurations(maxCommandDeduplicationDuration), ConfigTransforms.updateTargetTimestampForwardTolerance(24.hours), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .updateTestingConfig( _.focus(_.commitmentSendDelay).replace( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala index da391bef40..3817ac298f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/acs/commitment/util/CommitmentTestUtil.scala @@ -379,32 +379,74 @@ trait CommitmentTestUtil protected def awaitNextTick( participant: LocalParticipantReference, - counterparticipant: ParticipantReference, - )(implicit env: TestConsoleEnvironment, intervalDuration: IntervalDuration): CommitmentPeriod = { + counterParticipant: LocalParticipantReference, + )(implicit + env: TestConsoleEnvironment, + intervalDuration: IntervalDuration, + ): CommitmentPeriod = { import env.* val simClock = environment.simClock.value val tick1 = tickAfter(simClock.uniqueTime()) simClock.advanceTo(tick1.forgetRefinement.immediateSuccessor) + // Await the synchronizer time. Internally this will trigger a fetch of the synchronizer time. participant.testing.await_synchronizer_time(daId, tick1.forgetRefinement.immediateSuccessor) + counterParticipant.testing.await_synchronizer_time( + daId, + tick1.forgetRefinement.immediateSuccessor, + ) - val p1Computed = eventually() { - val p1Computed = participant.commitments.computed( + val participantComputed = eventually() { + val participantComputed = participant.commitments.computed( daName, tick1.toInstant.minusMillis(1), tick1.toInstant, - Some(counterparticipant.id), + Some(counterParticipant.id), ) - p1Computed should have size 1L - p1Computed + participantComputed should have size 1L + + val counterParticipantComputed = counterParticipant.commitments.computed( + daName, + tick1.toInstant.minusMillis(1), + tick1.toInstant, + Some(participant.id), + ) + counterParticipantComputed should have size 1L + + participantComputed } - val (period, _participant, commitment) = p1Computed.loneElement + // the values are the same for the participant and counter participant, but it is better to wait for both in wallClock time + val (period, _participantId, commitment) = participantComputed.loneElement period } - protected def checkReceivedCommitment( + protected def checkSentCommitmentTo( + recipients: Seq[ParticipantReference] + )( + period: CommitmentPeriod, + participant: ParticipantReference, + synchronizer: SynchronizerId, + expected: Int = 1, + ): Unit = eventually() { + val timeRange = + TimeRange(period.fromExclusive.forgetRefinement, period.toInclusive.forgetRefinement) + val sentCommitments = participant.commitments.lookup_sent_acs_commitments( + synchronizerTimeRanges = Seq(SynchronizerTimeRange(synchronizer, Some(timeRange))), + counterParticipants = Seq.empty, + commitmentState = Seq.empty, + verboseMode = false, + ) + + val sentCommitmentsOnSynchronizer = sentCommitments.get(synchronizer).value + sentCommitmentsOnSynchronizer.size should be >= expected + sentCommitmentsOnSynchronizer.map( + _.destCounterParticipant.uid + ) should contain theSameElementsAs (recipients.map(_.uid)) + } + + protected def checkReceivedCommitments( period: CommitmentPeriod, participant: ParticipantReference, synchronizer: SynchronizerId, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala index f851f09fde..7f98b54fe8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/benchmarks/ReplayingParticipant.scala @@ -267,6 +267,7 @@ object ReplayingParticipant extends FutureHelpers with EitherValues with OptionV testedReleaseProtocolVersion, futureSupervisor, clock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala index 24f071cfb0..973ae97d4b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/bftsynchronizer/ReassignmentTest.scala @@ -28,7 +28,7 @@ trait ReassignmentTest extends CommunityIntegrationTest with SharedEnvironment { override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P5S4M4_Manual - .addConfigTransform(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransform(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) protected val sequencerGroups: MultiSynchronizer = MultiSynchronizer( Seq( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala index 5d53112e61..cac15cd3a8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/BftSequencerConnectionsIntegrationTest.scala @@ -14,6 +14,7 @@ import com.digitalasset.canton.annotations.UnstableTest import com.digitalasset.canton.config.DbConfig import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.InstanceReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -106,8 +107,11 @@ sealed trait BftSequencerConnectionsIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (sequencers.remote, - /* trust threshold */ PositiveInt.two, /* liveness margin */ NonNegativeInt.zero) + mediator1 -> MediatorSequencersConfiguration( + sequencers.remote, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala index 6c4e691301..0bdcc4ef80 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/ConnectionPoolHealthIntegrationTest.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.admin.api.client.data.{ } import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.{InstanceReference, LocalInstanceReference} +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -46,8 +47,11 @@ sealed trait ConnectionPoolHealthIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (sequencers.local, - /* trust threshold */ PositiveInt.two, /* liveness margin */ NonNegativeInt.one) + mediator1 -> MediatorSequencersConfiguration( + sequencers.local, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.one, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala index da67689410..8f01ab9b4f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/connection/SequencerConnectionServiceIntegrationTest.scala @@ -11,6 +11,7 @@ import com.digitalasset.canton.admin.api.client.data.{ } import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.InstanceReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -61,8 +62,11 @@ sealed trait SequencerConnectionServiceIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (Seq(sequencer1, sequencer2), - /* trust threshold */ PositiveInt.one, /* liveness margin */ NonNegativeInt.zero) + mediator1 -> MediatorSequencersConfiguration( + Seq(sequencer1, sequencer2), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala index aa9c237624..75f26753ea 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/AcsCommitmentCrashIntegrationTest.scala @@ -152,6 +152,11 @@ class AcsCommitmentCrashIntegrationTest IouSyntax.createIou(participant1)(participant1.adminParty, participant2.adminParty) val period = awaitNextTick(participant1, participant2) - checkReceivedCommitment(period, participant2, daId, Match) + + // Check that we sent out the commitments (even if the send delay is zero) + checkSentCommitmentTo(Seq(participant2))(period, participant1, daId) + checkSentCommitmentTo(Seq(participant1))(period, participant2, daId) + + checkReceivedCommitments(period, participant2, daId, Match) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala index b68df2a2da..be215539fc 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/MediatorFailoverIntegrationTest.scala @@ -21,9 +21,9 @@ import com.digitalasset.canton.integration.plugins.{ } import com.digitalasset.canton.integration.tests.* import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, EnvironmentDefinition, - EnvironmentSetup, EnvironmentSetupPlugin, SharedEnvironment, TestConsoleEnvironment, @@ -34,7 +34,7 @@ import com.digitalasset.canton.sequencing.client.SequencerClient trait MediatorFailoverIntegrationTest extends ReliabilityTestSuite with ReplicatedMediatorTestSetup { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def startAndGet(external: UseExternalProcess)( mediatorName: String diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala index 158cbafa07..71b1ea8ef1 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/ParticipantRestartTest.scala @@ -84,6 +84,7 @@ import com.digitalasset.canton.integration.util.{EntitySyntax, PartiesAllocator} import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors.SubmissionAlreadyInFlight import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.networking.Endpoint import com.digitalasset.canton.participant.ParticipantNodeParameters import com.digitalasset.canton.participant.admin.inspection.SyncStateInspection @@ -391,6 +392,7 @@ abstract class ParticipantRestartTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), @@ -610,7 +612,7 @@ class ParticipantRestartCausalityIntegrationTest extends ParticipantRestartTest EnvironmentDefinition.P4S2M2_Manual .addConfigTransforms( ConfigTransforms.updateTargetTimestampForwardTolerance(30.seconds), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => NetworkBootstrapper(EnvironmentDefinition.S1M1_S1M1) @@ -975,7 +977,7 @@ class ParticipantRestartRealClockIntegrationTest extends ParticipantRestartTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3S2M2_Manual .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ProgrammableSequencer.configOverride(getClass.toString, loggerFactory), ) @@ -1589,7 +1591,7 @@ class ParticipantRestartRealClockIntegrationTest extends ParticipantRestartTest } abstract class ParticipantRestartStaticTimeIntegrationTestBase( - alphaMultiSynchronizerSupport: Boolean = false + enableAllLedgerApiReassignments: Boolean = false ) extends ParticipantRestartTest { private val overrideMaxRequestSize = NonNegativeInt.tryCreate(100 * 1024) @@ -1608,7 +1610,8 @@ abstract class ParticipantRestartStaticTimeIntegrationTestBase( _.focus(_.sequencerClient.overrideMaxRequestSize).replace(Some(overrideMaxRequestSize)) ), ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), ) .withSetup { implicit env => @@ -1746,7 +1749,7 @@ abstract class ParticipantRestartStaticTimeIntegrationTestBase( participant1.repair.purge(daName, Seq(baselineContractId), ignoreAlreadyPurged = false) - val (repairOffset, repairRecordTime) = if (alphaMultiSynchronizerSupport) { + val (repairOffset, repairRecordTime) = if (enableAllLedgerApiReassignments) { participant1.ledger_api.updates .reassignments( Set(party), @@ -2199,7 +2202,7 @@ class ParticipantRestartStaticTimeIntegrationTest @UnstableTest // TODO(#30408) class ParticipantRestartStaticTimeReassignmentIntegrationTest - extends ParticipantRestartStaticTimeIntegrationTestBase(alphaMultiSynchronizerSupport = true) + extends ParticipantRestartStaticTimeIntegrationTestBase(enableAllLedgerApiReassignments = true) @nowarn("msg=match may not be exhaustive") class ParticipantRestartContractKeyIntegrationTest extends ParticipantRestartTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala index 2bb28e2e51..c007974e86 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/crashrecovery/SequencerRestartTest.scala @@ -350,7 +350,7 @@ abstract class BaseSynchronizerRestartTest } -class SequencerRestartTest +final class SequencerRestartTest extends BaseSynchronizerRestartTest with FlagCloseable with HasCloseContext { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala index a0b61472f4..1702cc7577 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/ExampleIntegrationTest.scala @@ -8,9 +8,9 @@ import com.digitalasset.canton.ConsoleScriptRunner import com.digitalasset.canton.config.CantonConfig import com.digitalasset.canton.console.BufferedProcessLogger import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.environment.Environment +import com.digitalasset.canton.environment.CantonEnvironment import com.digitalasset.canton.integration.{ - BaseIntegrationTest, + CantonBaseIntegrationTest, ConfigTransform, EnvironmentDefinition, IsolatedEnvironments, @@ -20,7 +20,7 @@ import com.digitalasset.canton.util.Mutex import com.digitalasset.canton.util.ShowUtil.* abstract class ExampleIntegrationTest(configPaths: File*) - extends BaseIntegrationTest + extends CantonBaseIntegrationTest with IsolatedEnvironments with HasConsoleScriptRunner { @@ -73,7 +73,7 @@ abstract class ExampleIntegrationTest(configPaths: File*) trait HasConsoleScriptRunner { this: NamedLogging => import org.scalatest.EitherValues.* - def runScript(scriptPath: File)(implicit env: Environment): Unit = + def runScript(scriptPath: File)(implicit env: CantonEnvironment): Unit = ConsoleScriptRunner.run(env, scriptPath.toJava, logger = logger).value.discard } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala index f0b792fc7c..9ad4c9cdb3 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/examples/MultisyncExampleIntegrationTest.scala @@ -14,7 +14,7 @@ import com.digitalasset.canton.integration.bootstrap.{ } import com.digitalasset.canton.integration.util.MultiSynchronizerFeatureFlag import com.digitalasset.canton.integration.{ - BaseIntegrationTest, + CantonBaseIntegrationTest, CommunityIntegrationTest, ConfigTransforms, EnvironmentDefinition, @@ -26,7 +26,7 @@ import org.slf4j.event.Level import scala.sys.process.Process class MultisyncExampleIntegrationTest - extends BaseIntegrationTest + extends CantonBaseIntegrationTest with IsolatedEnvironments with CommunityIntegrationTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala index 344eb6402c..3bd8c748bb 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/health/RemoteDumpIntegrationTest.scala @@ -14,7 +14,7 @@ import com.digitalasset.canton.console.{ HealthDumpGenerator, InstanceReference, } -import com.digitalasset.canton.environment.{Environment, EnvironmentFactory} +import com.digitalasset.canton.environment.{CantonEnvironment, CantonEnvironmentFactory} import com.digitalasset.canton.integration.plugins.{ UseBftSequencer, UseExternalProcess, @@ -334,13 +334,13 @@ class NegativeRemoteDumpIntegrationTest registerPlugin(new UseBftSequencer(loggerFactory)) // Customize the environment factory to tweak the health dump generation - override protected val environmentFactory: EnvironmentFactory = + override protected val environmentFactory: CantonEnvironmentFactory = ( config: CantonConfig, loggerFactory: NamedLoggerFactory, testingConfigInternal: TestingConfigInternal, ) => - new Environment( + new CantonEnvironment( config, testingConfigInternal, ParticipantNodeBootstrapFactoryImpl, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala new file mode 100644 index 0000000000..161eaac2c9 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsInitIntegrationTest.scala @@ -0,0 +1,162 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.ledgerapi + +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, NonNegativeLong} +import com.digitalasset.canton.examples.java.iou.Dummy +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.{ + CommunityIntegrationTest, + ConfigTransform, + ConfigTransforms, + EnvironmentDefinition, + IsolatedEnvironments, + TestConsoleEnvironment, +} +import com.digitalasset.canton.logging.SuppressionRule +import com.digitalasset.canton.platform.indexer.IndexerConfig.AchsConfig +import com.digitalasset.canton.util.ResourceUtil.withResource +import monocle.macros.syntax.lens.* +import org.slf4j.event.Level + +import scala.concurrent.duration.* +import scala.concurrent.{Await, Future} +import scala.jdk.CollectionConverters.* + +trait AchsInitIntegrationTest extends CommunityIntegrationTest with IsolatedEnvironments { + + override lazy val environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P1_S1M1 + .addConfigTransforms( + ConfigTransforms.disableAchs, + ConfigTransforms.disableAdditionalConsistencyChecks, + ) + + private val achsLogRule: SuppressionRule = + SuppressionRule.LoggerNameContains("InitializeParallelIngestion") && SuppressionRule + .LevelAndAbove(Level.INFO) + + private def stopAllNodes(implicit env: TestConsoleEnvironment): Unit = { + import env.* + participants.all.synchronizers.disconnect(daName) + nodes.local.stop() + } + + "ACHS initialization is interrupted when participant is shut down during startup" when { + "shutdown is triggered via participant.stop()" in { implicit env => + runAchsInitInterruptionTest { participant => + participant.stop() + } + } + + "shutdown is triggered via participant.close()" in { implicit env => + runAchsInitInterruptionTest { participant => + // Bypass Nodes.stopAndWait and close the bootstrap directly. This + // exercises the isClosing-driven cancellation path (no + // cancelInitializationHint() call): OnShutdownRunner.close() flips + // isClosing as its first action, which the ACHS init pipe observes + // via externalShutdownSignal. + val bootstrap = participant.consoleEnvironment.environment.participants + .getStarting(participant.name) + .getOrElse(fail(s"participant ${participant.name} is not in StartingUp state")) + bootstrap.close() + } + } + } + + private def runAchsInitInterruptionTest( + triggerShutdown: com.digitalasset.canton.console.LocalParticipantReference => Unit + )(implicit env: TestConsoleEnvironment): Unit = { + import env.* + + participant1.synchronizers.connect_local(sequencer1, daName) + + participant1.dars.upload(CantonTestsPath) + + val alice = participant1.parties.enable("Alice") + + // Generate a lot of ACHS work (while ACHS is disabled) so that the + // eventual ACHS initialization, with `initParallelism = 1` and + // `initAggregationThreshold = 1`, takes a meaningful amount of time and + // can be reliably interrupted by a shutdown mid-init. + val createAndArchiveDummy = + new Dummy(alice.toProtoPrimitive) + .createAnd() + .exerciseArchive() + .commands + .asScala + .toSeq + val commandsPerTx = 100 + val txCount = 20 + for (i <- 1 to txCount) { + participant1.ledger_api.javaapi.commands.submit( + Seq(alice), + (1 to commandsPerTx).flatMap(_ => createAndArchiveDummy), + commandId = s"setup-dummy-$i", + ) + } + + val slowInitAchsConfig = AchsConfig( + validAtDistanceTarget = NonNegativeLong.tryCreate(10L), + lastPopulatedDistanceTarget = NonNegativeLong.tryCreate(5L), + aggregationThreshold = 5L, + initParallelism = NonNegativeInt.tryCreate(1), + initAggregationThreshold = 1L, + ) + val slowInitAchsTransform: ConfigTransform = + ConfigTransforms.updateAllParticipantConfigs_( + _.focus(_.parameters.ledgerApiServer.indexer.achsConfig) + .replace(Some(slowInitAchsConfig)) + ) + // Force manual start so we drive participant startup ourselves and can race + // a shutdown against ACHS initialization. + val manualStartTransform: ConfigTransform = + c => c.copy(parameters = c.parameters.copy(manualStart = true)) + + stopAllNodes(env) + val newEnv = manualCreateEnvironmentWithPreviousState( + env.actualConfig, + _ => manualStartTransform(slowInitAchsTransform(env.actualConfig)), + ) + withResource(newEnv) { achsEnv => + import achsEnv.* + + sequencer1.start() + mediator1.start() + + val interruptedMessage = "ACHS snapshot initialization interrupted by shutdown request" + loggerFactory.assertLogsSeq(achsLogRule)( + { + // Start the participant in the background, start() blocks on full init, + // which in turn blocks on ACHS initialization completing. + val startF = Future(participant1.start()) + + eventually(60.seconds, maxPollInterval = 100.millis) { + loggerFactory.fetchRecordedLogEntries.exists( + _.message.contains("Initializing ACHS snapshot") + ) shouldBe true + } + + triggerShutdown(participant1) + scala.util.Try(Await.result(startF, 5.minute)) + }, + logEntries => { + val interruptedLogs = + logEntries.filter(_.message.contains(interruptedMessage)) + withClue( + s"Expected '$interruptedMessage' log message during shutdown. " + + s"All ACHS-related log entries:\n${logEntries.map(_.message).mkString("\n")}" + ) { + interruptedLogs should not be empty + } + }, + ) + } + } +} + +class AchsInitIntegrationTestPostgres extends AchsInitIntegrationTest { + registerPlugin(new UsePostgres(loggerFactory)) + registerPlugin(new UseBftSequencer(loggerFactory)) +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala index d27ecc8683..254fa23ac2 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsIntegrationTest.scala @@ -52,8 +52,8 @@ trait AchsIntegrationTest extends CommunityIntegrationTest with SharedEnvironmen storageBackendFactory.createParameterStorageBackend(ledgerApiStore.stringInterningView) ledgerApiStore.ledgerApiDbSupport.dbDispatcher .executeSql( - DatabaseMetrics.ForTesting("fetchACHSState") - )(parameterStorageBackend.fetchACHSState)( + DatabaseMetrics.ForTesting("fetchAchsState") + )(parameterStorageBackend.fetchAchsState)( LoggingContextWithTrace.empty ) .futureValue diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala index d5a78be1f3..544717a02a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/AchsRepairIntegrationTest.scala @@ -50,8 +50,8 @@ sealed trait AchsRepairIntegrationTest extends RepairServiceIntegrationTest { storageBackendFactory.createParameterStorageBackend(ledgerApiStore.stringInterningView) ledgerApiStore.ledgerApiDbSupport.dbDispatcher .executeSql( - DatabaseMetrics.ForTesting("fetchACHSState") - )(parameterStorageBackend.fetchACHSState)( + DatabaseMetrics.ForTesting("fetchAchsState") + )(parameterStorageBackend.fetchAchsState)( LoggingContextWithTrace.empty ) .futureValue diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala index 3590dbc093..60a5db52e9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiParticipantPruningTest.scala @@ -38,11 +38,17 @@ import com.digitalasset.canton.ledger.error.LedgerApiErrors.ParticipantContractP import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.ParticipantPruningInProgress import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil +import com.digitalasset.canton.platform.store.backend.DataSourceStorageBackend.DataSourceConfig +import com.digitalasset.canton.platform.store.backend.common.ComposableQuery.SqlStringInterpolation +import com.digitalasset.canton.platform.store.backend.common.QueryStrategy +import com.digitalasset.canton.platform.store.cache.MutableLedgerEndCache +import com.digitalasset.canton.platform.store.interning.MockStringInterning import com.digitalasset.canton.topology.SynchronizerId import com.digitalasset.daml.lf.value.Value.ContractId import monocle.macros.syntax.lens.* import org.slf4j.event +import java.sql.Connection import java.time.Duration as JDuration import java.util.UUID import java.util.concurrent.atomic.AtomicReference @@ -94,7 +100,7 @@ trait LedgerApiParticipantPruningTest ConfigTransforms.useStaticTime, ConfigTransforms.updateMaxDeduplicationDurations(transactionTolerance.asJava), lowerLedgerApiServerBatchSize, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -174,8 +180,7 @@ trait LedgerApiParticipantPruningTest participant1.pruning.get_offset_by_time(tsOfLastPrunedEvent.toInstant) // Simulate concurrent pruning from another replica by issuing the pruning lock - val lockedPruning = - participant1.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore.lockPruning + val lockedPruning = withConnectionForTest(participant1)(lockPruning(participant1)) // Prune and remember offsets. val pruneF = Future(participant1.pruning.prune(offsetAtTheBeginning)) @@ -194,16 +199,14 @@ trait LedgerApiParticipantPruningTest lockedPruning.commitAndClose() pruneF.futureValue participant1.testing.state_inspection.internalContractIdOf(cidBeginningContractId) shouldBe None - participant1.testing.state_inspection.internalContractIdOf( - cidMiddleContractId - ) should not be empty + val cidMiddleInternalContractIdOpt = + participant1.testing.state_inspection.internalContractIdOf(cidMiddleContractId) + cidMiddleInternalContractIdOpt should not be empty // Simulate blocking pruning by read locking one of the to-be-pruned contracts - val contractLock = - participant1.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore - .readLockContract( - participant1.testing.state_inspection.internalContractIdOf(cidMiddleContractId).value - ) + val contractLock = withConnectionForTest(participant1)( + readLockContract(participant1, cidMiddleInternalContractIdOpt.value) + ) // Pruning fails if contract pruning cannot resolve the optimistic lock after retries loggerFactory.assertThrowsAndLogs[CommandFailure]( @@ -465,31 +468,28 @@ trait LedgerApiParticipantPruningTest val c1InternalContractId = participant1.testing.state_inspection.internalContractIdOf(c1ContractId).value - val (_, c2cid) = createContract(participant1, daId) - val c2ContractId = ContractId.assertFromString(c2cid) - val c2InternalContractId = - participant1.testing.state_inspection.internalContractIdOf(c2ContractId).value - - // this is needed for the locking approach to work later - c1InternalContractId should be < (c2InternalContractId) - // unassign val unassign = participant1.ledger_api.commands.submit_unassign( submitter = participant1.adminParty, - contractIds = Seq(c1ContractId, c2ContractId), + contractIds = Seq(c1ContractId), source = daId, target = acmeId, ) val pruningOffset = participant1.ledger_api.state.end() - // issue write lock on participant1 for C1, this will block the Indexer at ingestion of the following assignation on the first contract - val c1Lock = - participant1.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore - .writeLockContract(c1InternalContractId) + // issue write lock on participant1 for C1, this will block the Indexer at ingestion of the following assignation on the contract + val c1Lock = withConnectionForTest(participant1)( + testFunction = writeLockContract(participant1, c1InternalContractId), + onCommit = conn => { + // simulate pruning by manually remove C1 from the contract store + deleteContract(participant1, c1InternalContractId)(conn) + logger.info("C1 contract removed") + }, + ) logger.info("C1 locked") - // reassign C1 and C2 to acme, this should be blocked on Indexing the assignment because of the lock above + // reassign C1 to acme, this should be blocked on Indexing the assignment because of the lock above val reassignmentF = Future( participant1.ledger_api.commands.submit_assign( submitter = participant1.adminParty, @@ -498,16 +498,11 @@ trait LedgerApiParticipantPruningTest target = acmeId, ) ) - logger.info("Reassignment of C1,C2 started") + logger.info("Reassignment of C1 started") // wait a little to make sure the assignment is already blocked Threading.sleep(5000) - logger.info("Waited 5 second") - - // simulate pruning by manually remove C2 from the contract store - participant1.testing.state_inspection.deleteContract(c2InternalContractId) - participant1.testing.state_inspection.internalContractIdOf(c2ContractId) shouldBe None - logger.info("C2 contract removed") + logger.info("Waited 5 seconds") loggerFactory.assertLogsSeq( SuppressionRule.Level(event.Level.INFO) && @@ -530,10 +525,10 @@ trait LedgerApiParticipantPruningTest }, ) - // C2 is reinserted - val c2NewInternalContractId = - participant1.testing.state_inspection.internalContractIdOf(c2ContractId).value - c2InternalContractId should be < (c2NewInternalContractId) + // C1 is reinserted + val c1NewInternalContractId = + participant1.testing.state_inspection.internalContractIdOf(c1ContractId).value + c1InternalContractId should be < (c1NewInternalContractId) // pruning before assign so that referential integrity is restored waitUntilSafeToPrune(participant1, Some(pruningOffset)) @@ -689,6 +684,60 @@ trait LedgerApiParticipantPruningTest .value offer.id.exerciseAcceptByPainter().commands.loneElement } + + private def withConnectionForTest( + participant: LocalParticipantReference + )(testFunction: Connection => Unit, onCommit: Connection => Unit = _ => ()) = { + val ledgerApiStore = + participant.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore + val conn = + ledgerApiStore.ledgerApiDbSupport.storageBackendFactory.createDataSourceStorageBackend + .createDataSource( + dataSourceConfig = DataSourceConfig(ledgerApiStore.ledgerApiStorage.jdbcUrl), + loggerFactory = loggerFactory, + ) + .getConnection + conn.setAutoCommit(false) + QueryStrategy.withoutNetworkTimeout(testFunction(_))(conn, noTracingLogger) + new Object { + def commitAndClose(): Unit = { + onCommit(conn) + conn.commit() + conn.close() + } + } + } + + private def eventStorageBackend(participant: LocalParticipantReference) = + participant.underlying.value.sync.participantNodePersistentState.value.ledgerApiStore.ledgerApiDbSupport.storageBackendFactory + .createEventStorageBackend( + ledgerEndCache = MutableLedgerEndCache(), + stringInterning = new MockStringInterning, + loggerFactory = loggerFactory, + ) + + private def lockPruning(participant: LocalParticipantReference)(conn: Connection) = + eventStorageBackend(participant).lockExclusivelyPruningProcessingTable(conn) + + private def readLockContract(participant: LocalParticipantReference, internalContractId: Long)( + conn: Connection + ) = eventStorageBackend(participant).readLockInternalContractIds(Set(internalContractId))(conn) + + private def writeLockContract(participant: LocalParticipantReference, internalContractId: Long)( + conn: Connection + ) = + eventStorageBackend(participant).writeLockInternalContractIds(cSQL"= $internalContractId")(conn) + + def deleteContract(participant: LocalParticipantReference, internalContractId: Long)(implicit + conn: Connection + ): Int = { + val removed = SQL"DELETE FROM par_contracts WHERE internal_contract_id=$internalContractId" + .executeUpdate()(conn) + participant.underlying.value.sync.participantNodePersistentState.value.contractStore + .contractsPruned(List(internalContractId)) + removed + } + } class LedgerApiParticipantPruningTestPostgres extends LedgerApiParticipantPruningTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala new file mode 100644 index 0000000000..afdaf642d2 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/GetCompletionsAuthIT.scala @@ -0,0 +1,118 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.ledgerapi.auth + +import com.daml.grpc.test.StreamConsumer +import com.daml.ledger.api.v2.command_completion_service.{ + CommandCompletionServiceGrpc, + CompletionStreamResponse, + GetCompletionsRequest, +} +import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits.* +import com.digitalasset.canton.integration.TestConsoleEnvironment +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UseH2} +import com.digitalasset.canton.integration.tests.ledgerapi.services.SubmitAndWaitDummyCommand +import io.grpc.stub.StreamObserver + +import scala.concurrent.Future + +final class GetCompletionsAuthIT + extends ExpiringStreamServiceCallAuthTests[CompletionStreamResponse] + with SubmitAndWaitDummyCommand { + registerPlugin(new UseH2(loggerFactory)) + registerPlugin(new UseBftSequencer(loggerFactory)) + + override def serviceCallName: String = "CommandCompletionService#GetCompletions" + + override protected def stream( + context: ServiceCallContext, + env: TestConsoleEnvironment, + ): StreamObserver[CompletionStreamResponse] => Unit = + streamFor(context) + + private def mkRequest(parties: List[String]) = + GetCompletionsRequest(parties, 0) + + private def streamFor( + context: ServiceCallContext + ): StreamObserver[CompletionStreamResponse] => Unit = + observer => + stub(CommandCompletionServiceGrpc.stub(channel), context.token) + .getCompletions(mkRequest(List(context.mainActorId)), observer) + + override def serviceCall(context: ServiceCallContext)(implicit + env: TestConsoleEnvironment + ): Future[Any] = { + import env.* + val mainActorId = getMainActorId + submitAndWaitAsMainActor(mainActorId).flatMap(_ => + new StreamConsumer[CompletionStreamResponse]( + streamFor(context.copy(mainActorId = mainActorId)) + ).first() + ) + } + + serviceCallName should { + "allow calls with valid parties" taggedAs securityAsset + .setHappyCase( + "Ledger API client can make a GetCompletions call for its own parties" + ) in { implicit env => + import env.* + expectSuccess(serviceCall(canActAsMainActor)) + } + + "deny calls with empty parties for user without CanReadAsAnyParty" taggedAs securityAsset + .setAttack( + attackPermissionDenied(threat = + "Present a JWT without CanReadAsAnyParty and request completions with empty parties" + ) + ) in { implicit env => + import env.* + val mainActorId = getMainActorId + expectPermissionDenied( + submitAndWaitAsMainActor(mainActorId).flatMap { _ => + new StreamConsumer[CompletionStreamResponse](observer => + stub(CommandCompletionServiceGrpc.stub(channel), canActAsMainActor.token) + .getCompletions(mkRequest(List.empty), observer) + ).first() + } + ) + } + + "allow calls with empty parties for CanReadAsAnyParty user" taggedAs securityAsset + .setHappyCase( + "Ledger API client with CanReadAsAnyParty can call GetCompletions with empty parties" + ) in { implicit env => + import env.* + val mainActorId = getMainActorId + expectSuccess( + submitAndWaitAsMainActor(mainActorId).flatMap { _ => + new StreamConsumer[CompletionStreamResponse](observer => + stub(CommandCompletionServiceGrpc.stub(channel), canReadAsAnyParty.token) + .getCompletions(mkRequest(List.empty), observer) + ).first() + } + ) + } + + "deny calls requesting a party the user is not authorized for" taggedAs securityAsset + .setAttack( + attackPermissionDenied(threat = + "Present a JWT authorized for the main actor and request completions for a foreign party" + ) + ) in { implicit env => + import env.* + val mainActorId = getMainActorId + val foreignParty = getRandomPartyId + expectPermissionDenied( + submitAndWaitAsMainActor(mainActorId).flatMap { _ => + new StreamConsumer[CompletionStreamResponse](observer => + stub(CommandCompletionServiceGrpc.stub(channel), canActAsMainActor.token) + .getCompletions(mkRequest(List(foreignParty)), observer) + ).first() + } + ) + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala index 94be66f9d3..667f5d22b0 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/auth/IDPBoxingServiceCallOutTests.scala @@ -141,6 +141,46 @@ trait IDPBoxingServiceCallOutTests } } + "IDP admin granting rights that transcend IDP boundaries" should { + for ( + (description, kind) <- List[(String, uproto.Right.Kind)]( + ( + "read as any party", + uproto.Right.Kind.CanReadAsAnyParty(uproto.Right.CanReadAsAnyParty()), + ), + ( + "execute as any party", + uproto.Right.Kind.CanExecuteAsAnyParty(uproto.Right.CanExecuteAsAnyParty()), + ), + ( + "participant admin", + uproto.Right.Kind.ParticipantAdmin(uproto.Right.ParticipantAdmin()), + ), + ) + ) { + s"deny granting $description rights" taggedAs adminSecurityAsset + .setAttack( + attackUnknownResource(threat = s"Grant $description rights") + ) in { implicit env => + import env.* + loggerFactory.suppress(AuthServiceJWTSuppressionRule) { + expectPermissionDenied { + val suffix = UUID.randomUUID().toString + for { + (_, idpAdminContext, _) <- createIDPBundle(canBeAnAdmin, suffix) + + _ <- boxedCall( + "user-" + suffix, + idpAdminContext, + Vector(uproto.Right(kind)), + ) + } yield () + } + } + } + } + } + "allow Admin granting permissions to parties which do not exist" taggedAs adminSecurityAsset .setHappyCase( "Grant rights to non existing parties" diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala index d31bc18f2a..01a57cacc7 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/fixture/CantonFixture.scala @@ -8,10 +8,10 @@ import com.digitalasset.canton.config.{AuthServiceConfig, PositiveDurationSecond import com.digitalasset.canton.console.LocalParticipantReference import com.digitalasset.canton.integration.tests.ledgerapi.auth.SandboxRequiringAuthorizationFuns import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, EnvironmentDefinition, - EnvironmentSetup, IsolatedEnvironments, SharedEnvironment, TestConsoleEnvironment, @@ -47,7 +47,7 @@ trait CantonFixtureIsolated trait CantonFixtureAbstract extends CommunityIntegrationTest with SandboxRequiringAuthorizationFuns { - this: EnvironmentSetup => + this: CantonEnvironmentSetup => protected def darFile = new File(CantonTestsPath) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala index 00ac40d346..6344e3fb5a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/ExternalPartyOnboardingIntegrationTest.scala @@ -9,7 +9,9 @@ import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.console.CommandFailure import com.digitalasset.canton.console.commands.PartiesAdministration +import com.digitalasset.canton.crypto.KeyPurpose.Signing import com.digitalasset.canton.crypto.{SigningKeyUsage, SigningKeysWithThreshold} +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.MediatorError import com.digitalasset.canton.integration.{ CommunityIntegrationTest, @@ -17,16 +19,21 @@ import com.digitalasset.canton.integration.{ EnvironmentDefinition, HasCycleUtils, SharedEnvironment, + TestConsoleEnvironment, } import com.digitalasset.canton.logging.LogEntry import com.digitalasset.canton.participant.topology.ParticipantTopologyManagerError.ExternalPartyAlreadyExists import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId -import com.digitalasset.canton.topology.transaction.DelegationRestriction.CanSignAllMappings +import com.digitalasset.canton.topology.transaction.DelegationRestriction.{ + CanSignAllButNamespaceDelegations, + CanSignAllMappings, +} import com.digitalasset.canton.topology.transaction.ParticipantPermission.{ Confirmation, Observation, } import com.digitalasset.canton.topology.transaction.{ + DelegationRestriction, HostingParticipant, MultiTransactionSignature, NamespaceDelegation, @@ -85,6 +92,179 @@ class ExternalPartyOnboardingIntegrationTest extends ExternalPartyOnboardingInte .party shouldBe patrick.partyId } + "sign the external party transaction for the participant" when { + // This test case checks that a participant signs external party allocations only for itself, even if + // the participant owns a signing key with a delegation from another participant. This restriction + // is in place to avoid a loophole where a participant might not want to host an external party, + // but through a namespace delegation, the external party might try to acquire the participant's signature + // through other means (e.g. by submitting the allocation via a participant with a namespace delegation in place). + "the participant also has a delegated key for another participant" in { implicit env => + import env.* + + // set up the key delegation from participant2 to a key owned by participant1 + val delegatedKey = + participant1.keys.secret.generate_signing_key(usage = Set(SigningKeyUsage.Namespace)) + participant2.topology.namespace_delegations.propose_delegation( + participant2.namespace, + delegatedKey, + DelegationRestriction.CanSignSpecificMappings(PartyToParticipant.code), + store = daId, + ) + + eventually() { + participant1.topology.namespace_delegations + .list( + daId, + filterNamespace = participant2.namespace.filterString, + filterTargetKey = Some(delegatedKey.fingerprint), + ) + .loneElement + .discard + } + // validate that the delegation from participant2 to participant1's key actually works for PTPs + participant1.parties.enable( + "party-on-p2", + namespace = participant2.namespace, + synchronizer = daName, + ) + + val partyId = allocateExternalParty().partyId + + val ptp = eventually() { + participant2.topology.party_to_participant_mappings + .list(daId, proposals = true, filterParty = partyId) + .loneElement + } + // future-proofing the test: ensure that the PTP has both participants as hosting participants (and therefore eligigle for signing it) + ptp.item.participantIds should contain theSameElementsAs Seq( + participant1.id, + participant2.id, + ) + // check that participant1 only signed with the key for participant1, even though it owns a delegated key for participant2 + ptp.context.signedBy.forgetNE.loneElement shouldBe participant1.fingerprint + + // clean up: revoke the delegation again + participant2.topology.namespace_delegations.propose_revocation( + participant2.namespace, + targetKey = delegatedKey, + store = daId, + ) + + eventually() { + participant1.topology.namespace_delegations.list( + daId, + filterNamespace = participant2.namespace.filterString, + filterTargetKey = Some(delegatedKey.fingerprint), + ) shouldBe empty + } + + } + "the participant has an offline root key" in { implicit env => + import env.* + + // download the root key so that we can restore it at the end of the test + val rootNamespaceKey = participant1.keys.secret.download(participant1.fingerprint) + + // delete the root namespace key to make it "offline" + participant1.keys.secret.delete(participant1.fingerprint, force = true) + + // try to allocate the external party without any valid topology signing key + loggerFactory.assertThrowsAndLogs[CommandFailure]( + allocateExternalParty(), + _.errorMessage should include( + "Could not find an appropriate signing key to issue the topology transaction" + ), + ) + + // temporarily restore the root namespace key and issue a namespace delegation + // for an intermediate key + participant1.keys.secret.upload(rootNamespaceKey, name = None) + + val intermediateKey = + participant1.keys.secret.generate_signing_key(usage = Set(SigningKeyUsage.Namespace)) + participant1.topology.namespace_delegations.propose_delegation( + participant1.namespace, + intermediateKey, + CanSignAllButNamespaceDelegations, + store = daId, + ) + + eventually() { + participant1.topology.namespace_delegations + .list(daId, filterTargetKey = Some(intermediateKey.fingerprint)) + .loneElement + } + + // delete the root namespace key again + participant1.keys.secret.delete(participant1.fingerprint, force = true) + + // allocating the external party should work now + val externalPartyId = allocateExternalParty().partyId + + // validate that the mapping for the external party exists, + // and that it was only signed by intermediate key + eventually() { + participant1.topology.party_to_participant_mappings + .list( + daId, + proposals = true, + filterParty = externalPartyId, + ) + .loneElement + .context + .signedBy + .forgetNE should contain theSameElementsAs Seq(intermediateKey.fingerprint) + } + + participant1.keys.secret.upload(rootNamespaceKey, name = None) + } + } + + /** Allocates an external party with a random name in sequencer1's namespace with participant1 + * and participant2 as the hosting participants. + */ + def allocateExternalParty()(implicit env: TestConsoleEnvironment) = { + import env.* + participant1.ledger_api.parties + .allocate_external( + daId, + Seq( + TopologyTransaction( + TopologyChangeOp.Replace, + PositiveInt.one, + PartyToParticipant.tryCreate( + PartyId.tryCreate(UUID.randomUUID().toString, sequencer1.namespace), + PositiveInt.one, + Seq( + HostingParticipant(participant1.id, ParticipantPermission.Confirmation), + HostingParticipant(participant2.id, ParticipantPermission.Confirmation), + ), + partySigningKeysWithThreshold = Some( + SigningKeysWithThreshold( + NonEmpty( + Set, + // pick some key as the party's signing key + sequencer1.keys.public + .list( + filterPurpose = Set(Signing), + filterUsage = Set(SigningKeyUsage.Protocol), + ) + .head + .publicKey + .asSigningKey + .value, + ), + PositiveInt.one, + ) + ), + ), + testedProtocolVersion, + ) -> Seq.empty + ), + Seq.empty, + ) + } + "allocate a party with a PartyToKeyMapping" in { implicit env => import env.* val namespaceKey = global_secret.keys.secret diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala index 37fff2258d..4a96d7348d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/submission/InteractiveSubmissionVersioningIntegrationTest.scala @@ -13,7 +13,8 @@ import com.digitalasset.canton.version.{HashingSchemeVersion, ProtocolVersion} import java.util.UUID -class InteractiveSubmissionVersioningIntegrationTest extends InteractiveSubmissionNuckSetupTest { +final class InteractiveSubmissionVersioningIntegrationTest + extends InteractiveSubmissionNuckSetupTest { "Interactive submission" should { val supportedHashingSchemeVersions: Set[HashingSchemeVersion] = diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala index c58331d5fb..13918eb5a4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftOrderingBenchmark.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.integration.tests.manual +import com.digitalasset.canton.config.ReplicationConfig import com.digitalasset.canton.config.RequireTypes.{ NonNegativeInt, Port, @@ -66,6 +67,7 @@ import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} * -Dscala.concurrent.context.numThreads=30 \ * -Dbft-ordering-benchmark.num-db-connections-per-node=5 \ * -Dbft-ordering-benchmark.transaction-sizes-and-weights={payloads=[{size-bytes=2000,weight=1}]} \ + * -Dbft-ordering-benchmark.test-catchup={nodes-to-stop=[2],duration-nodes-are-down=1minutes,duration-node-need-to-startup=10seconds}\ * -Dbft-ordering-benchmark.benchmark-duration=1minute" * * export CI=1 # When this defined, it ensures no dockerized Postgres is being used @@ -158,7 +160,7 @@ class BftOrderingBenchmark PositiveInt.tryCreate( Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.num-db-connections-per-node")) .map(_.toInt) - .getOrElse(5) + .getOrElse(12) ) /** Tracing options. Disabled by default. */ @@ -228,6 +230,27 @@ class BftOrderingBenchmark Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.sequencer-db-latency-millis")) .map(_.toLong) + /** Whether DB replication (`DbMultiStorage`) is enabled. Default is [[Some(true)]]. To disable, + * set to [[Some(false)]]. Note that `DbMultiStorage` uses a separate connection pool, reserving + * roughly half of the total DB connections available in the [[num-db-connections-per-node]]. + */ + private val dbReplicationEnabled: Option[Boolean] = + Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.db-replication-enabled")) + .map(_.toBoolean) + .orElse(Some(true)) + + private val testCatchupConfig: BftBenchmarkConfig.TestCatchup = + Option(System.getProperty(s"$BFTOrderingBenchmarkPrefix.test-catchup")) + .map { s => + val result = + ConfigSource + .string(s) + .load[BftBenchmarkConfig.TestCatchup] + result.left.foreach(errors => logger.error(s"Failed to parse testCatchup config: $errors")) + result.getOrElse(throw new RuntimeException("Invalid test catchup configuration")) + } + .getOrElse(BftBenchmarkConfig.TestCatchup.NoTestCatchup) + registerPlugin( new UsePostgres( loggerFactory, @@ -279,6 +302,15 @@ class BftOrderingBenchmark ) } }) + .addConfigTransforms( + ConfigTransforms.updateAllSequencerConfigs { case (_, config) => + dbReplicationEnabled.fold(config) { replicationEnabled => + config + .focus(_.replication) + .replace(Some(ReplicationConfig(enabled = Some(replicationEnabled)))) + } + } + ) .addConfigTransforms( _.focus(_.monitoring.tracing.tracer).replace( TracingConfig.Tracer( @@ -377,36 +409,49 @@ class BftOrderingBenchmark waitUntilAllBftSequencersAuthenticateDisseminationQuorum(5.minutes) + val nodesToStop = env.sequencers.local.zipWithIndex + .filter(x => testCatchupConfig.nodesToStop.contains(x._2)) + .map(_._1) + + if (nodesToStop.nonEmpty) { + + nodesToStop.foreach(_.stop()) + + env.actorSystem.scheduler.scheduleOnce(testCatchupConfig.durationNodesAreDown) { + nodesToStop.foreach(_.start()) + } + } + val benchmarkTool = new BftBenchmarkTool(new DaBftBindingFactory(loggerFactory), loggerFactory) + val p2pEndpoints = bftSequencerPlugin.p2pEndpoints.getOrElse(fail("No P2P endpoints found")) val benchmarkToolConfig = BftBenchmarkConfig( transactionSizesAndWeights = transactionSizesAndWeights.payloads, + testCatchup = testCatchupConfig, runDuration = runDuration, perNodeWritePeriod = perNodeWritePeriod, reportingInterval = reportingIntervalOpt, - nodes = bftSequencerPlugin.p2pEndpoints - .getOrElse(fail("No P2P endpoints found")) - .values - .zipWithIndex - .map { case (p2pConfig, idx) => - val host = p2pConfig.address - val port = p2pConfig.port.unwrap - val node: BftBenchmarkConfig.Node = - if (idx == 0) { - BftBenchmarkConfig.NetworkedReadWriteNode( - host = host, - writePort = port, - readPort = port, - ) - } else { - BftBenchmarkConfig.NetworkedWriteOnlyNode( - host = host, - writePort = port, - ) - } - node - } - .toSeq, + nodes = env.sequencers.local.zipWithIndex.map { case (sequencer, idx) => + val name = sequencer.name + val p2pConfig = p2pEndpoints(name) + + val host = p2pConfig.address + val port = p2pConfig.port.unwrap + val node: BftBenchmarkConfig.Node = + if (idx == 0) { + BftBenchmarkConfig.NetworkedReadWriteNode( + host = host, + writePort = port, + readPort = port, + ) + } else { + BftBenchmarkConfig.NetworkedWriteOnlyNode( + host = host, + writePort = port, + ) + } + node + }, ) benchmarkTool.run(benchmarkToolConfig).discard } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala index d20bb417e9..d27c6a1d96 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/BftSingleNodePerformanceTest.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.console.{ LocalSequencerReference, } import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -128,7 +129,11 @@ class BftSingleNodePerformanceTest mediators = allMediators_, overrideMediatorToSequencers = Some( allMediators_.map { mediator => - mediator -> (Seq(sequencer1), PositiveInt.one, NonNegativeInt.zero) + mediator -> MediatorSequencersConfiguration( + Seq(sequencer1), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) }.toMap ), mediatorThreshold = PositiveInt.one, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala index 758e54fab8..1bfe5463dd 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/DataContinuityTest.scala @@ -39,9 +39,7 @@ import com.digitalasset.canton.integration.{ SharedEnvironment, TestConsoleEnvironment, } -import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality import com.digitalasset.canton.logging.{LogEntry, TracedLogger} -import com.digitalasset.canton.resource.DatabaseStorageError import com.digitalasset.canton.synchronizer.sequencer.ProgrammableSequencer import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext @@ -103,34 +101,6 @@ trait DataContinuityTest // Set to true if you want to persist the dumps locally even if a test container is found lazy val forceLocalDumps = false - // Suppress flaky DB task rejection warnings during outer environment transitions and final teardown. - // When nodes are stopped, Slick connection pools terminate immediately ("Terminated"). If background - // processes (like the ACS Commitment Processor running heavy SQL queries) are still executing, - // subsequent pool submissions trigger benign DB_STORAGE_DEGRADATION warnings caused by underlying - // RejectedExecutionException failures. - // - // TODO(#16601): This is a workaround for an uncoordinated shutdown sequence - // A coordinated graceful shutdown that actively cancels ongoing queries would make this suppression obsolete. - protected def assertBenignDbStorageDegradationShutdown(e: LogEntry): Assertion = - e.shouldBeCantonError( - DatabaseStorageError.DatabaseStorageDegradation, - messageAssertion = msg => { - // Check the top-level message text - msg should include("A database task was rejected from the database task queue") - }, - contextAssertion = mdc => { - // Inspect the raw error context dumped by Slick into the MDC map - val slickMsg = mdc.getOrElse("messageFromSlick", "") - slickMsg should include("RejectedExecutionException") - // These strings originate from java.util.concurrent.ThreadPoolExecutor#toString(). - // We match only shutdown phases to suppress benign teardown races, while ensuring - // actual load-based queue rejections ("Running") remain visible. - slickMsg should include regex "Shutting down|Terminated" - }, - loggerAssertion = - loggerName => loggerName should startWith("com.digitalasset.canton.resource.DbStorageMulti"), - ) - override val logsToBeHandledAtStartup: Option[Seq[LogEntry] => Assertion] = Some( LogEntry.assertLogSeq( Seq.empty, @@ -143,8 +113,6 @@ trait DataContinuityTest s"Using a session signing key is not possible with protocol version 34." ) ), - // Flake prevention: Suppress trailing DB task rejections during mid-test dump restorations via loadState - assertBenignDbStorageDegradationShutdown, ), ) ) @@ -154,12 +122,7 @@ trait DataContinuityTest protocolVersion: ProtocolVersion, )(f: TestConsoleEnvironment => Unit): Unit = { - // Flake prevention: Catch benign DB rejections when stopping the old environment. - // Wrapped separately to avoid nesting suppression scopes. - loggerFactory.assertLogsUnorderedOptional( - oldEnv.nodes.local.foreach(_.stop()), - LogEntryOptionality.OptionalMany -> assertBenignDbStorageDegradationShutdown, - ) + oldEnv.nodes.local.foreach(_.stop()) val newEnv = manualCreateEnvironment( initialConfig = oldEnv.environment.config, configTransform = config => @@ -173,12 +136,7 @@ trait DataContinuityTest logger.info(s"About to run with protocol version $protocolVersion") f(newEnv) } finally { - // Flake prevention: Catch benign DB rejections during final environment teardown. - // Wrapped separately so mid-test calls to `handleStartupLogs` do not trigger nested suppression errors. - loggerFactory.assertLogsUnorderedOptional( - destroyEnvironment(newEnv), - LogEntryOptionality.OptionalMany -> assertBenignDbStorageDegradationShutdown, - ) + destroyEnvironment(newEnv) } } @@ -727,16 +685,11 @@ trait SynchronizerChangeDataContinuityTest extends SynchronizerChangeDataContinu val unassignedEvent = incompleteUnassignedEvents.loneElement // act on state clue("starting assignment and paint offer acceptance") { - // Flake prevention: Catch trailing DB task rejections if background processors - // flake concurrently with active test execution. - loggerFactory.assertLogsUnorderedOptional( - assignmentAndPaintOfferAcceptance( - Alice.toPartyId(), - Bank.toPartyId(), - Painter.toPartyId(), - unassignedEvent.entry.getUnassignedEvent, - ), - LogEntryOptionality.OptionalMany -> assertBenignDbStorageDegradationShutdown, + assignmentAndPaintOfferAcceptance( + Alice.toPartyId(), + Bank.toPartyId(), + Painter.toPartyId(), + unassignedEvent.entry.getUnassignedEvent, ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala index eba41b394f..6048be328e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/S3Synchronization.scala @@ -203,7 +203,7 @@ object S3Synchronization { final case class ContinuityDumpS3Ref(override val path: String) extends ContinuityDumpRef { lazy val localDownloadPath: File = { val syncCommand = - s"aws s3 sync s3://canton-public-releases/data-continuity-dumps/$path ${baseDbDumpPath.path}/$path --no-sign-request" + s"aws s3 sync s3://canton-public-releases/data-continuity-dumps/$path ${baseDbDumpPath.path}/$path --no-sign-request --quiet" val syncResult = runSynchronized(syncCommand) if (syncResult != 0) { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala index ae183ad196..bf8a0adfe2 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/manual/topology/TopologyStateVerification.scala @@ -281,6 +281,7 @@ class TopologyStateVerification( ReleaseProtocolVersion(BaseTest.testedProtocolVersion), futureSupervisor, clock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala new file mode 100644 index 0000000000..3a75ef82bd --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/metrics/SequencerConnectionPoolMetricsIntegrationTest.scala @@ -0,0 +1,232 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.metrics + +import com.daml.metrics.api.MetricQualification +import com.digitalasset.canton.admin.api.client.data.{ + GrpcSequencerConnection, + SubmissionRequestAmplification, +} +import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} +import com.digitalasset.canton.console.{ + InstanceReference, + LocalInstanceReference, + LocalMediatorReference, + LocalParticipantReference, +} +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration +import com.digitalasset.canton.integration.bootstrap.{ + NetworkBootstrapper, + NetworkTopologyDescription, +} +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.{ + CommunityIntegrationTest, + EnvironmentDefinition, + SharedEnvironment, +} +import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} +import com.digitalasset.canton.{SequencerAlias, UniquePortGenerator, config} +import monocle.macros.syntax.lens.* + +import scala.concurrent.duration.* + +/** This test checks that the sequencer connection pool metrics properly obtain a `psid` label in + * their context. + * + * The environment is as follows: + * - 2 participants + * - 4 sequencers + * - 1 mediator + * + * The participants and the mediator connect to all sequencers with a trust threshold of 4. + * + * The test first restarts all the participants because the `psid` is known only starting at the + * second connect. + * + * The test then pings participant2 from participant1, and proceeds to examine all the connection + * pool metrics for all participant and mediator nodes, validating that they have the `psid` label + * where expected. + */ +final class SequencerConnectionPoolMetricsIntegrationTest + extends CommunityIntegrationTest + with SharedEnvironment { + registerPlugin(new UsePostgres(loggerFactory)) + registerPlugin(new UseBftSequencer(loggerFactory)) + + override def environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P2S4M1_Config + .addConfigTransform( + _.focus(_.monitoring.metrics) + .replace( + MetricsConfig( + qualifiers = MetricQualification.All, + reporters = Seq( + MetricsReporterConfig.Prometheus( + port = UniquePortGenerator.next + ) + ), + ) + ) + ) + .withNetworkBootstrap { implicit env => + import env.* + + new NetworkBootstrapper( + NetworkTopologyDescription( + daName, + synchronizerOwners = Seq[InstanceReference](sequencer1, mediator1), + synchronizerThreshold = PositiveInt.one, + sequencers = sequencers.local, + mediators = Seq(mediator1), + overrideMediatorToSequencers = Some( + Map( + mediator1 -> MediatorSequencersConfiguration( + sequencers.local, + trustThreshold = PositiveInt.four, + livenessMargin = NonNegativeInt.zero, + ) + ) + ), + ) + ) + } + .withSetup { implicit env => + import env.* + + val amplification = SubmissionRequestAmplification( + factor = 20, + patience = config.NonNegativeFiniteDuration.tryFromDuration(1.seconds), + ) + + Seq(participant1, participant2).foreach( + _.synchronizers.connect_bft( + sequencers.local.map(s => + GrpcSequencerConnection.fromInternal( + s.config.publicApi.clientConfig + .asSequencerConnection(SequencerAlias.tryCreate(s.name), sequencerId = None) + ) + ), + synchronizerAlias = daName, + sequencerTrustThreshold = PositiveInt.four, + sequencerLivenessMargin = NonNegativeInt.zero, + submissionRequestAmplification = amplification, + ) + ) + } + + "SequencerConnectionPoolMetrics" should { + "have the `psid` label after the first connect" in { implicit env => + import env.* + + val nodes = Seq[LocalInstanceReference](participant1, participant2, mediator1) + + // For participants, the `psid` will be populated starting at the second connect + clue("disconnect and reconnect participants") { + participants.all.foreach(_.synchronizers.disconnect_all()) + participants.all.foreach(_.synchronizers.reconnect_all()) + } + + participant1.health.ping(participant2) + + clue("check metrics") { + val psidKeyName = "psid" + val prefix = "daml.sequencer-client.sequencer-connection-pool" + + forAll(nodes) { node => + val metrics = node.metrics.list(prefix) + // Ensure the test fails if we change the prefix, which would result in an empty `metrics` + metrics.size should be >= 8 + + node match { + case _: LocalMediatorReference => + // Mediators have their `psid` always defined, even at their first connection + forAll(metrics) { case (_name, values) => + values.size should be >= 1 + forAll(values)(_.attributes should contain key psidKeyName) + } + + case _: LocalParticipantReference => + val uniqueMetrics = Seq( + "trust-threshold", + "tracked-connections", + "validated-connections", + "subscription-threshold", + "active-subscriptions", + ).map(m => s"$prefix.$m") + + forAll(metrics) { + case (name, values) if uniqueMetrics.contains(name) => + // These metrics don't have separate instances for different sets of labels, so they will all have the `psid` + values.loneElement.attributes should contain key psidKeyName + + case (name, values) + if name == s"$prefix.connection-health" || name == s"$prefix.subscription-health" => + // The metrics on the first connect will be without `psid`, but they are closed when disconnecting. + // Metrics for the second connect will have the `psid`. + // With 4 sequencers and trust threshold = 4, we will have 4 connections and 4 subscriptions. + values should have size 4 + forAll(values)(_.attributes should contain key psidKeyName) + + case (name, values) if name == s"$prefix.grpc-requests" => + // These metrics are counters and are not closed when the pool closes, so the metrics without `psid` will be around + + val metricsPerEndPoint = values.groupBy(_.attributes("endpoint")) + val endpointsWithPsidOnAllConnections = Seq( + "GetApiInfo", + "GetSynchronizerId", + "GetSynchronizerParameters", + "Handshake", + "Authenticate", + "Challenge", + "Subscribe", + ) + val endpointsWithPsidOnSomeConnections = Seq( + "SendAsync", + "AcknowledgeSigned", + ) + + val endpointsWithoutPsid = Seq( + "DownloadTopologyStateForInit", + "DownloadTopologyStateForInitHash", + ) + + val endpointsNotCalled = Seq( + "Logout", + "GetTime", + "GetTrafficStateForMember", + ) + + forAll(metricsPerEndPoint) { + case (endpoint, metrics) + if endpointsWithPsidOnAllConnections.contains(endpoint) => + // All the connections will have a `psid` because all the connections use these endpoints + forExactly(4, metrics)(_.attributes should contain key psidKeyName) + + case (endpoint, metrics) + if endpointsWithPsidOnSomeConnections.contains(endpoint) => + // Not all connections will necessarily have a `psid` because not all connections may use these endpoints + forAtLeast(1, metrics)(_.attributes should contain key psidKeyName) + + case (endpoint, metrics) if endpointsWithoutPsid.contains(endpoint) => + // There are no metric with `psid` because these endpoints are only used during the first connection + forAll(metrics)(_.attributes should not(contain key psidKeyName)) + + case (endpoint, _) if endpointsNotCalled.contains(endpoint) => + // These endpoints should not be called during this test + fail(s"endpoint should not have been called: $endpoint") + + case (endpoint, _) => fail(s"unknown endpoint: $endpoint") + } + + case (other, _) => fail(s"unexpected metric: $other") + } + + case _ => fail(s"unexpected node type: $node") + } + } + } + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala index ae61cb77ed..5e67e4d947 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/AutomaticReassignmentDecentralizedPartyIntegrationTest.scala @@ -48,7 +48,7 @@ class AutomaticReassignmentDecentralizedPartyIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala index acc499ffb3..ae46ba543f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/DivulgenceIntegrationTestHelpers.scala @@ -27,7 +27,7 @@ object DivulgenceIntegrationTestHelpers { case object NonConsumed extends EventType implicit class ParticipantSimpleStreamHelper(val participant: LocalParticipantReference)(implicit - val alphaMultiSynchronizerSupport: Boolean = false + val enableAllLedgerApiReassignments: Boolean = false ) { def acs(party: Party): Seq[OffsetCid] = @@ -111,7 +111,7 @@ object DivulgenceIntegrationTestHelpers { parties: Seq[PartyId], beginOffsetExclusive: Long, ): Seq[UpdateService.UpdateWrapper] = { - val reassignmentsFilter = if (alphaMultiSynchronizerSupport) { + val reassignmentsFilter = if (enableAllLedgerApiReassignments) { Some( EventFormat( filtersByParty = parties.map(party => party.toLf -> Filters(Nil)).toMap, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala index e2b761a7dc..08c1bbf38d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/OnlinePartyReplicationParticipantProtocolTest.scala @@ -89,7 +89,7 @@ sealed trait OnlinePartyReplicationParticipantProtocolTest ConfigTransforms .enableAlphaOnlinePartyReplicationSupport(enableUnsafeSequencerChannelSupport = true)* ) - .addConfigTransform(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransform(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* // More frequent ACS commitments by configuring a smaller reconciliation interval. diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala index 56d5a760c4..c40b4795d2 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/DivulgenceIntegrationTest.scala @@ -16,10 +16,10 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa import com.digitalasset.canton.integration.tests.multihostedparties.DivulgenceIntegrationTestHelpers.* // Whether to use Assign/Unassign (multi-synchronizer) or Create/Archive for the ACS import - def alphaMultiSynchronizerSupport: Boolean + def enableAllLedgerApiReassignments: Boolean // Inject this setting into the implicit scope for the helper class - implicit def alphaSupportImplicit: Boolean = alphaMultiSynchronizerSupport + implicit def alphaSupportImplicit: Boolean = enableAllLedgerApiReassignments // Make sure deduplication duration does not block pruning private val maxDedupDuration = java.time.Duration.ofSeconds(2) @@ -29,7 +29,8 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa super.environmentDefinition .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), ConfigTransforms.updateMaxDeduplicationDurations(maxDedupDuration), ) @@ -100,7 +101,7 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa participant: LocalParticipantReference, contractId: String, party: Party, - ) = if (alphaMultiSynchronizerSupport) { + ) = if (enableAllLedgerApiReassignments) { assertEventNotFound(participant, contractId, party) } else { checkCreatedEventFor(participant, contractId, party) @@ -525,13 +526,13 @@ trait DivulgenceIntegrationTest extends OfflinePartyReplicationIntegrationTestBa immediateDivulged2P2Import, ) // event query - if (alphaMultiSynchronizerSupport) + if (enableAllLedgerApiReassignments) assertEventNotFound(participant2, aliceStakeholderCreatedP1.contractId, alice) else checkCreatedEventFor(participant2, aliceStakeholderCreatedP1.contractId, alice) checkCreatedEventFor(participant2, aliceBobStakeholderCreatedP1.contractId, alice) checkCreatedEventFor(participant2, divulgeIouByExerciseP1.contractId, alice) assertEventNotFound(participant2, immediateDivulged1P1.contractId, alice) - if (alphaMultiSynchronizerSupport) + if (enableAllLedgerApiReassignments) assertEventNotFound(participant2, immediateDivulged2P1.contractId, alice) else checkCreatedEventFor(participant2, immediateDivulged2P1.contractId, alice) assertEventNotFound(participant2, immediateDivulged1ArchiveP1.contractId, alice) @@ -596,18 +597,18 @@ trait DivulgenceIntegrationTestWithoutCache extends DivulgenceIntegrationTest { } class DivulgenceIntegrationTestReassignmentWithCache extends DivulgenceIntegrationTest { - override def alphaMultiSynchronizerSupport: Boolean = true + override def enableAllLedgerApiReassignments: Boolean = true } class DivulgenceIntegrationTestLegacyWithCache extends DivulgenceIntegrationTest { - override def alphaMultiSynchronizerSupport: Boolean = false + override def enableAllLedgerApiReassignments: Boolean = false } class DivulgenceIntegrationTestReassignmentWithoutCache extends DivulgenceIntegrationTestWithoutCache { - override def alphaMultiSynchronizerSupport: Boolean = true + override def enableAllLedgerApiReassignments: Boolean = true } class DivulgenceIntegrationTestLegacyWithoutCache extends DivulgenceIntegrationTestWithoutCache { - override def alphaMultiSynchronizerSupport: Boolean = false + override def enableAllLedgerApiReassignments: Boolean = false } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala index b5a16bff8e..9b35bfdc89 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multihostedparties/offpr/WorkflowIdsIntegrationTest.scala @@ -24,15 +24,15 @@ import java.util.Collections * custom-configured. */ abstract class WorkflowIdsIntegrationTestBase( - alphaMultiSynchronizerSupport: Boolean = false + enableAllLedgerApiReassignments: Boolean = false ) extends OfflinePartyReplicationIntegrationTestBase { override lazy val environmentDefinition: EnvironmentDefinition = super.environmentDefinition .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport) - .replace(alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ) ) @@ -113,7 +113,7 @@ abstract class WorkflowIdsIntegrationTestBase( party: Party, expectedCount: Int, ): Seq[NormalizedEvent] = - if (alphaMultiSynchronizerSupport) { + if (enableAllLedgerApiReassignments) { val reassignments = target.ledger_api.updates .reassignments(Set(party), completeAfter = PositiveInt.tryCreate(expectedCount)) reassignments.map { r => @@ -158,4 +158,4 @@ abstract class WorkflowIdsIntegrationTestBase( final class WorkflowIdsIntegrationTest extends WorkflowIdsIntegrationTestBase final class WorkflowIdsReassignmentIntegrationTest - extends WorkflowIdsIntegrationTestBase(alphaMultiSynchronizerSupport = true) + extends WorkflowIdsIntegrationTestBase(enableAllLedgerApiReassignments = true) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala index 4c5c10b24f..472cba7f35 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AssignmentBeforeUnassignmentIntegrationTest.scala @@ -47,7 +47,7 @@ sealed trait AssignmentBeforeUnassignmentIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala index e9947701a9..ae20129a5b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AsynchronousReassignmentProtocolIntegrationTest.scala @@ -70,7 +70,7 @@ final class AsynchronousReassignmentProtocolIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala index e547fa6096..15ca2767f9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/AutomaticReassignmentBatchingIntegrationTest.scala @@ -36,7 +36,7 @@ class AutomaticReassignmentBatchingIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S1M1_S1M1 .addConfigTransforms( ProgrammableSequencer.configOverride(this.getClass.toString, loggerFactory), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala index df6de065ee..5c2f4cc189 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationAdminPartyIntegrationTest.scala @@ -69,7 +69,7 @@ sealed trait ReassignmentConfirmationAdminPartyIntegrationTest // Because we play with the simClock, ensure we have enough forward tolerance // on the target timestamp to not impact up unassigments. ConfigTransforms.updateTargetTimestampForwardTolerance(1.hours), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala index 20ac9e3605..dee97c89a8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentConfirmationPoliciesIntegrationTest.scala @@ -44,7 +44,7 @@ sealed trait ReassignmentConfirmationPoliciesPartyIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala index 22bc701f7c..bbf047740f 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentNoReassignmentDataIntegrationTest.scala @@ -63,7 +63,7 @@ sealed trait ReassignmentNoReassignmentDataIntegrationTest EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.useStaticTime, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala index 29ef10f11b..eb98e0ec9d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceConcurrentReassignmentsIntegrationTest.scala @@ -56,7 +56,7 @@ trait ReassignmentServiceConcurrentReassignmentsIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala index e1bc0b2f47..d0e8f1ad50 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceIntegrationTest.scala @@ -92,7 +92,7 @@ abstract class ReassignmentServiceIntegrationTest .addConfigTransforms( // Ensure reassignments are not tripped up by some participants being a little behind. ConfigTransforms.updateTargetTimestampForwardTolerance(30.seconds), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala index 22880f7a07..5b0a766f16 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentServiceTimeoutCommandRejectedIntegrationTest.scala @@ -77,7 +77,7 @@ sealed trait ReassignmentServiceTimeoutCommandRejectedIntegrationTest .addConfigTransforms( ConfigTransforms.useStaticTime, ConfigTransforms.updateTargetTimestampForwardTolerance(60.seconds), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala index 1743a5e421..c408e6b71d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentSubmissionIntegrationTest.scala @@ -52,7 +52,7 @@ sealed trait ReassignmentSubmissionIntegrationTest // We want to trigger time out .addConfigTransforms( ConfigTransforms.useStaticTime, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala index f9b74820ca..2086552209 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentTargetTimestampIntegrationTest.scala @@ -45,7 +45,7 @@ class ReassignmentTargetTimestampIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala index e0de9be45f..f48da31091 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationObserversIntegrationTest.scala @@ -85,7 +85,7 @@ sealed trait ReassignmentsConfirmationObserversIntegrationTest // We want to trigger time out .addConfigTransforms( ConfigTransforms.useStaticTime, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala index ab4d6ab16b..f56a3509ac 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/ReassignmentsConfirmationThresholdIntegrationTest.scala @@ -91,7 +91,7 @@ sealed trait ReassignmentsConfirmationThresholdIntegrationTest .addConfigTransforms( ConfigTransforms.useStaticTime, ConfigTransforms.updateTargetTimestampForwardTolerance(10.minutes), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala index 42b589d7fb..4960f5679d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/RepairServiceIntegrationTest.scala @@ -50,7 +50,7 @@ abstract class RepairServiceIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S2M1_S2M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala index 7a68b905c3..d6c5f53838 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/multisynchronizer/UpdateServiceIntegrationTest.scala @@ -47,7 +47,7 @@ abstract class UpdateServiceIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1_S2M1_S2M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala index 84742c2630..3995e2ac87 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/kms/KmsCryptoNoPreDefinedKeysIntegrationTest.scala @@ -15,8 +15,8 @@ import com.digitalasset.canton.crypto.store.{CryptoPrivateStore, KmsCryptoPrivat import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UseKms} import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown @@ -28,7 +28,7 @@ import scala.concurrent.Future * keys (i.e. keys are generated on-the-fly using a KMS and nodes are automatically initialized). */ trait KmsCryptoNoPreDefinedKeysIntegrationTest extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def kmsConfig: KmsConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala index 22b926d6ce..e0aaaebce8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/nightly/sequencer/SequencerCatchUpPerformanceIntegrationTest.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.console.{ LocalSequencerReference, } import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -72,7 +73,7 @@ import scala.concurrent.duration.* * Check the events/s in the `log` folder after the run: * * {{{ - * cat canton_test.log | grep 'sequencer2 events/s' + * cat canton_test.log | grep 'sequencer4 events/s' * }}} * * More options: @@ -137,7 +138,7 @@ class SequencerCatchUpPerformanceIntegrationTest // Enable to see metrics in Prometheus or Grafana. For some reason, doesn't work with the 'countBlockEvents' flag private val exposeHttpMetrics = false - // Enable to automatically count events/s, grep 'sequencer2 events/s' after the test run + // Enable to automatically count events/s, grep 'sequencer4 events/s' after the test run // Set to true by default, so we can print the sequencer speed on CircleCI private val countBlockEvents = true @@ -150,13 +151,13 @@ class SequencerCatchUpPerformanceIntegrationTest // The duration of load producing before the restart. The bigger the number, the more events will accumulate to catch up with private val beforeRestartDurationMillis = 5 * 60000L - // The max time for the sequencer2 to catch up + // The max time for the sequencer4 to catch up private val afterRestartDurationMillis = beforeRestartDurationMillis / 2 - // Useful to isolate the sequencer2 performance + // Useful to isolate the sequencer4 performance private val stopOtherNodesDuringCatchUp = true - // Keep producing load after catch-up. May result in sequencer2 never catching up, depending on the other settings. + // Keep producing load after catch-up. May result in sequencer4 never catching up, depending on the other settings. private val produceLoadAfterRestart = false private val enableTrafficManagement = true @@ -215,7 +216,7 @@ class SequencerCatchUpPerformanceIntegrationTest Option.when(useExternalSequencerProcess)( new UseExternalProcess( loggerFactory, - externalSequencers = Set("sequencer2"), + externalSequencers = Set("sequencer4"), fileNameHint = this.getClass.getSimpleName, configTransforms = if (exposeHttpMetrics) Seq(metricsConfigTransform, streamInstrumentationConfigTransform) @@ -223,11 +224,11 @@ class SequencerCatchUpPerformanceIntegrationTest ) ) - private val sequencer2Proxy = "sequencer2-to-postgres" + private val sequencer4Proxy = "sequencer4-to-postgres" private val toxiproxyPluginOpt: Option[UseToxiproxy] = Option.when(useToxiProxy)( new UseToxiproxy( - ToxiproxyConfig(proxies = Seq(SequencerToPostgres(sequencer2Proxy, "sequencer2"))) + ToxiproxyConfig(proxies = Seq(SequencerToPostgres(sequencer4Proxy, "sequencer4"))) ) ) @@ -237,7 +238,7 @@ class SequencerCatchUpPerformanceIntegrationTest new UsePostgres( loggerFactory, customMaxConnectionsByNode = Some { - case "sequencer2" => PositiveInt.tryCreate(10).some + case "sequencer4" => PositiveInt.tryCreate(10).some case _ => PositiveInt.tryCreate(5).some }, ) @@ -256,7 +257,7 @@ class SequencerCatchUpPerformanceIntegrationTest EnvironmentDefinition .buildBaseEnvironmentDefinition( numParticipants = 2, - numSequencers = 2, + numSequencers = 4, numMediators = 5, ) .withManualStart @@ -273,6 +274,8 @@ class SequencerCatchUpPerformanceIntegrationTest allMediators().foreach(_.start()) sequencer1.start() + sequencer2.start() + sequencer3.start() startSequencer() participant1.start() @@ -285,27 +288,33 @@ class SequencerCatchUpPerformanceIntegrationTest ) sequencer1.health.wait_for_ready_for_initialization() + sequencer2.health.wait_for_ready_for_initialization() + sequencer3.health.wait_for_ready_for_initialization() - val seq2 = if (useExternalSequencerProcess) remoteSequencer2 else sequencer2 - seq2.health.wait_for_ready_for_initialization() + val seq4 = if (useExternalSequencerProcess) remoteSequencer4 else sequencer4 + seq4.health.wait_for_ready_for_initialization() } .withNetworkBootstrap { implicit env => import env.* - val seq2 = if (useExternalSequencerProcess) remoteSequencer2 else sequencer2 + val seq4 = if (useExternalSequencerProcess) remoteSequencer4 else sequencer4 val allMediators_ = allMediators() new NetworkBootstrapper( NetworkTopologyDescription( daName, - synchronizerOwners = Seq(sequencer1, seq2), + synchronizerOwners = Seq(sequencer1, sequencer2, sequencer3, seq4), synchronizerThreshold = PositiveInt.one, - sequencers = Seq(sequencer1, seq2), + sequencers = Seq(sequencer1, sequencer2, sequencer3, seq4), mediators = allMediators_, overrideMediatorToSequencers = Some( allMediators_.map { mediator => // Make sure both mediators are connected the first sequencer to avoid the SEQUENCER_SUBSCRIPTION_LOST warning // And flaky results (load produced super slowly from time to time) - mediator -> (Seq(sequencer1), PositiveInt.one, NonNegativeInt.zero) + mediator -> MediatorSequencersConfiguration( + Seq(sequencer1), + trustThreshold = PositiveInt.one, + livenessMargin = NonNegativeInt.zero, + ) }.toMap ), mediatorThreshold = allMediators_.length - 1, @@ -413,7 +422,7 @@ class SequencerCatchUpPerformanceIntegrationTest participant2.health.ping(participant1.id) participant1.health.ping(participant2.id) - testLogger.info("sequencer2 STOPPING") + testLogger.info("sequencer4 STOPPING") stopSequencer() runners.foreach(env.environment.addUserCloseable(_)) @@ -435,11 +444,11 @@ class SequencerCatchUpPerformanceIntegrationTest loggerFactory.assertLoggedWarningsAndErrorsSeq( { - testLogger.info("sequencer2 RESTARTING") + testLogger.info("sequencer4 RESTARTING") // enable db toxiproxy toxiproxyPluginOpt.foreach { toxiproxyPlugin => - val proxy = toxiproxyPlugin.runningToxiproxy.getProxy(sequencer2Proxy) + val proxy = toxiproxyPlugin.runningToxiproxy.getProxy(sequencer4Proxy) val client = proxy .valueOrFail("must be here") .underlying @@ -452,7 +461,7 @@ class SequencerCatchUpPerformanceIntegrationTest val before = Instant.now() val seq2EventCountBeforeOpt: Option[Long] = - if (exposeHttpMetrics || !countBlockEvents) None else Some(blockEventCount(sequencer2)) + if (exposeHttpMetrics || !countBlockEvents) None else Some(blockEventCount(sequencer4)) def instantToSeconds(instant: Instant): Double = instant.getEpochSecond.toDouble + (instant.getNano.toDouble / 1000_000_000) @@ -464,26 +473,26 @@ class SequencerCatchUpPerformanceIntegrationTest maxPollInterval = 50 milliseconds, ) { val seq1EventCount = blockEventCount(sequencer1) - val seq2EventCount = blockEventCount(sequencer2) + val seq4EventCount = blockEventCount(sequencer4) testLogger.info( - s"Checking if sequencer2 has caught up. sequencer1 events: $seq1EventCount, sequencer2 events: $seq2EventCount" + s"Checking if sequencer4 has caught up. sequencer1 events: $seq1EventCount, sequencer4 events: $seq4EventCount" ) // seq2 has caught up if it has seen all events. // it may see more in case of shutdown during async processing - val seq2CaughtUp = seq2EventCount >= seq1EventCount + val seq2CaughtUp = seq4EventCount >= seq1EventCount assert(seq2CaughtUp) if (seq2CaughtUp) { val now = Instant.now() - val processedEventCount = seq2EventCount - seq2EventCountBefore + val processedEventCount = seq4EventCount - seq2EventCountBefore val eventsPerSec = processedEventCount.toDouble / (instantToSeconds(now) - instantToSeconds(before)) val logMessage = - f"sequencer2 events/s: $eventsPerSec%.2f, processed events: $processedEventCount" + f"sequencer4 events/s: $eventsPerSec%.2f, processed events: $processedEventCount" testLogger.info(logMessage) // Ugly solution to print the sequencer speed on CircleCI @@ -494,7 +503,7 @@ class SequencerCatchUpPerformanceIntegrationTest case None => Threading.sleep(afterRestartDurationMillis) } - testLogger.info("sequencer2 COMPLETED") + testLogger.info("sequencer4 COMPLETED") if (turnOffDebugLogging) NodeLoggingUtil.setLevel(level = "DEBUG") }, @@ -533,11 +542,11 @@ class SequencerCatchUpPerformanceIntegrationTest externalProcessOpt match { case Some(externalProcess) => - externalProcess.start("sequencer2") - assert(externalProcess.isRunning("sequencer2")) + externalProcess.start("sequencer4") + assert(externalProcess.isRunning("sequencer4")) case None => - sequencer2.start() - assert(sequencer2.is_running) + sequencer4.start() + assert(sequencer4.is_running) } } @@ -546,11 +555,11 @@ class SequencerCatchUpPerformanceIntegrationTest externalProcessOpt match { case Some(externalProcess) => - externalProcess.kill("sequencer2") - assert(!externalProcess.isRunning("sequencer2")) + externalProcess.kill("sequencer4") + assert(!externalProcess.isRunning("sequencer4")) case None => - sequencer2.stop() - assert(!sequencer2.is_running) + sequencer4.stop() + assert(!sequencer4.is_running) } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala index 0cd3009160..5e6109e379 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/operations/ViewConsistencyTest.scala @@ -71,7 +71,9 @@ sealed trait ViewConsistencyTest -- no views for flyway or blocks tables table_name not in ('flyway_schema_history', 'blocks') and -- DEV version adds a column, but it doesn't have any significance, so let's ignore it - not (table_name = 'common_node_id' and column_name = 'test_column') + not (table_name = 'common_node_id' and column_name = 'test_column') and + -- exclude partition tables + table_name !~ '_p[0-9]+$$' group by table_name, column_name having count(column_name) != 2""".as[(String, String, String)], "select", diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala index 49e73cd7f1..b60bb33482 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pkgdars/PackageUsableMixin.scala @@ -7,14 +7,14 @@ import com.daml.ledger.javaapi.data.Command import com.digitalasset.canton.config import com.digitalasset.canton.console.ParticipantReference import com.digitalasset.canton.damltests.java.conflicttest.Many -import com.digitalasset.canton.integration.BaseIntegrationTest +import com.digitalasset.canton.integration.CantonBaseIntegrationTest import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import org.scalatest.Assertion import scala.jdk.CollectionConverters.* trait PackageUsableMixin { - this: BaseIntegrationTest => + this: CantonBaseIntegrationTest => protected def submitCommand( submittingParticipant: ParticipantReference, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala index 09fc47aa71..8fd8c69b87 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/LedgerPruningIntegrationTest.scala @@ -3,9 +3,12 @@ package com.digitalasset.canton.integration.tests.pruning +import anorm.SqlParser.int +import anorm.SqlStringInterpolation import com.daml.ledger.api.v2.event_query_service.GetEventsByContractIdResponse import com.daml.ledger.api.v2.transaction.Transaction import com.daml.ledger.api.v2.transaction_filter.TransactionShape.TRANSACTION_SHAPE_LEDGER_EFFECTS +import com.daml.metrics.DatabaseMetrics import com.digitalasset.canton.BigDecimalImplicits.* import com.digitalasset.canton.config.DbConfig import com.digitalasset.canton.config.RequireTypes.PositiveInt @@ -22,6 +25,7 @@ import com.digitalasset.canton.integration.plugins.{ import com.digitalasset.canton.integration.tests.examples.IouSyntax import com.digitalasset.canton.integration.tests.multihostedparties.DivulgenceIntegrationTestHelpers.ParticipantSimpleStreamHelper import com.digitalasset.canton.ledger.error.groups.RequestValidationErrors.OffsetOutOfRange +import com.digitalasset.canton.logging.LoggingContextWithTrace import com.digitalasset.canton.participant.admin.data.{ ContractImportMode, RepairContract, @@ -776,11 +780,19 @@ abstract class LedgerPruningIntegrationTest contract.contractId } val cid1 = pushContract("0.01").coid - val _ = pushContract("0.02") + val cid2 = pushContract("0.02").coid - // eventually the first should be available in the contract store + // eventually the two contracts should be available in the contract store and in the event store too eventually() { contractFor(participant1, daId, cid1).isDefined shouldBe true + contractFor(participant1, daId, cid2).isDefined shouldBe true + participant1.underlying.value.sync.ledgerApiIndexer.asEval.value.ledgerApiStore.value.ledgerApiDbSupport.dbDispatcher + .executeSql(DatabaseMetrics.ForTesting("getting-all-activations-with-workflow-id"))( + SQL"""select count(*) c + from lapi_events_activate_contract + where workflow_id like 'failedAddContractOperation%'""".as(int("c").single)(_) + )(LoggingContextWithTrace.ForTesting) + .futureValue shouldBe 2 } loggerFactory.assertLoggedWarningsAndErrorsSeq( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala index 79f73cdd44..5967c97d96 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/pruning/ReassignmentPruningIntegrationTest.scala @@ -62,7 +62,7 @@ sealed trait ReassignmentPruningIntegrationTest .addConfigTransforms( ConfigTransforms.useStaticTime, ConfigTransforms.updateMaxDeduplicationDurations(maxDedupDuration), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala index 8af1bde8ca..346d844216 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/AcsImportReassignmentCounterIntegrationTest.scala @@ -31,10 +31,10 @@ sealed trait AcsImportReassignmentCounterIntegrationTest EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) .replace(true) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala index 8cda7b22fb..a1a19458ac 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/ImportContractsIntegrationTest.scala @@ -29,7 +29,7 @@ trait ImportContractsIntegrationTestBase with SharedEnvironment with EntitySyntax { - protected def enableAlphaMultiSynchronizerSupport: Boolean + protected def enableAllLedgerApiReassignments: Boolean private var alice: PartyId = _ private var bob: PartyId = _ @@ -39,10 +39,10 @@ trait ImportContractsIntegrationTestBase EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport) - .replace(enableAlphaMultiSynchronizerSupport) + _.focus(_.parameters.enableAllLedgerApiReassignments) + .replace(enableAllLedgerApiReassignments) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -91,7 +91,7 @@ trait ImportContractsIntegrationTestBase "Importing an ACS" should { - s"handle contracts with non-zero reassignment counter (multi-synchronizer-support=$enableAlphaMultiSynchronizerSupport)" in { + s"handle contracts with non-zero reassignment counter (multi-synchronizer-support=$enableAllLedgerApiReassignments)" in { implicit env => import env.* @@ -129,7 +129,7 @@ trait ImportContractsIntegrationTestBase participant3.synchronizers.disconnect_all() - if (enableAlphaMultiSynchronizerSupport) { + if (enableAllLedgerApiReassignments) { participant3.repair.import_acs(daId, file.canonicalPath) val reassignedContract = participant3.ledger_api.state.acs @@ -157,11 +157,11 @@ trait ImportContractsIntegrationTestBase * The ACS import will simply drop contracts that are associated to a different synchronizer; * and logs that fact once. */ - s"import a multi-synchronizer ACS snapshot (multi-synchronizer-support=$enableAlphaMultiSynchronizerSupport)" in { + s"import a multi-synchronizer ACS snapshot (multi-synchronizer-support=$enableAllLedgerApiReassignments)" in { implicit env => import env.* - // Create several contracts for both synchronizers having reassignment counter zero (thus independent of enableAlphaMultiSynchronizerSupport) + // Create several contracts for both synchronizers having reassignment counter zero (thus independent of enableAllLedgerApiReassignments) val contractsDa = (1 to 2).map { _ => IouSyntax.createIou(participant1, synchronizerId = Some(daId))(charlie, bob) } @@ -208,7 +208,7 @@ trait ImportContractsIntegrationTestBase // 1. Non-zero reassignment counter in recovered contract // 2. Imports contract using Assign/Unassign events // 3. Recovery retains pre-existing active contracts - s"recover successfully ACS import mid-crash for a reassigned contract preserving pre-existing state (multi-synchronizer-support=$enableAlphaMultiSynchronizerSupport)" in { + s"recover successfully ACS import mid-crash for a reassigned contract preserving pre-existing state (multi-synchronizer-support=$enableAllLedgerApiReassignments)" in { implicit env => import env.* @@ -244,7 +244,7 @@ trait ImportContractsIntegrationTestBase participant3.synchronizers.disconnect_all() - if (enableAlphaMultiSynchronizerSupport) { + if (enableAllLedgerApiReassignments) { val contractInstance = participant1.underlying.value.sync.participantNodePersistentState.value.contractStore .lookup(cid) @@ -339,10 +339,10 @@ trait ImportContractsIntegrationTestBase } final class ImportContractsIntegrationTest extends ImportContractsIntegrationTestBase { - override protected def enableAlphaMultiSynchronizerSupport: Boolean = false + override protected def enableAllLedgerApiReassignments: Boolean = false } final class ImportContractsWithReassignmentIntegrationTest extends ImportContractsIntegrationTestBase { - override protected def enableAlphaMultiSynchronizerSupport: Boolean = true + override protected def enableAllLedgerApiReassignments: Boolean = true } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala index bc8bb9e5b1..7bc838391e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RepairServiceIntegrationTest.scala @@ -83,7 +83,7 @@ trait RepairServiceIntegrationTest EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.enableAdvancedCommands(FeatureFlag.Repair), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) override val defaultParticipant: String = "participant1" @@ -858,7 +858,7 @@ sealed trait RepairServiceIntegrationTestStableLf extends RepairServiceIntegrati import env.* // If multi-synchronizer support is enabled, the purge will be represented as an Unassigned event, otherwise as a synthetic Archive event - val multiSynchronizerSupport = participant1.config.parameters.alphaMultiSynchronizerSupport + val multiSynchronizerSupport = participant1.config.parameters.enableAllLedgerApiReassignments val eventFormat = EventFormat( filtersByParty = @@ -1098,24 +1098,24 @@ sealed trait WithMultiSynchronizerSupport extends RepairServiceIntegrationTest { .addConfigTransforms( ConfigTransforms.enableAdvancedCommands(FeatureFlag.Repair), ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.alphaMultiSynchronizerSupport).replace(true) + _.focus(_.parameters.enableAllLedgerApiReassignments).replace(true) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) } -/** Contains tests that ONLY work when alphaMultiSynchronizerSupport = true */ +/** Contains tests that ONLY work when enableAllLedgerApiReassignments = true */ sealed trait RepairServiceMultiSynchronizerTests extends RepairServiceIntegrationTest { "RepairServiceMultiSynchronizerTests" must { - "run test only with enabled `alphaMultiSynchronizerSupport`" in { implicit env => + "run test only with enabled `enableAllLedgerApiReassignments`" in { implicit env => import env.* participants.local.foreach { participant => assert( - participant.config.parameters.alphaMultiSynchronizerSupport, - s"alphaMultiSynchronizerSupport must be true for ${participant.name} in this test suite", + participant.config.parameters.enableAllLedgerApiReassignments, + s"enableAllLedgerApiReassignments must be true for ${participant.name} in this test suite", ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala index 110a4d0787..70eb37d37a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/repair/RollbackUnassignmentIntegrationTest.scala @@ -32,7 +32,7 @@ sealed trait RollbackUnassignmentIntegrationTest EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( ConfigTransforms.enableAdvancedCommands(FeatureFlag.Repair), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala new file mode 100644 index 0000000000..373b761946 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/CryptoMetricsIntegrationTest.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.security + +import com.daml.metrics.api.testing.MetricValues.* +import com.digitalasset.canton.crypto.provider.jce.JcePrivateCrypto +import com.digitalasset.canton.crypto.provider.kms.KmsPrivateCrypto +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoWithPreDefinedKeysIntegrationTest +import com.digitalasset.canton.integration.tests.security.kms.gcp.GcpKmsCryptoIntegrationTestBase +import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, + CommunityIntegrationTest, + SharedEnvironment, +} + +/** Integration tests verifying that cryptographic operations using KMS providers are correctly + * instrumented to correctly record crypto-related metrics. + * + * In particular, this suite ensures that signing and decryption latencies are captured, as well as + * KMS-related metrics. + */ +trait CryptoMetricsIntegrationTest extends KmsCryptoWithPreDefinedKeysIntegrationTest { + self: CommunityIntegrationTest & CantonEnvironmentSetup => + + override lazy val protectedNodes: Set[String] = Set("participant1") + + "signing, decryption latencies, and KMS metrics are recorded" in { implicit env => + import env.* + + participant1.crypto.privateCrypto.isInstanceOf[KmsPrivateCrypto] shouldBe true + participant2.crypto.privateCrypto.isInstanceOf[JcePrivateCrypto] shouldBe true + + assertPingSucceeds(participant1, participant1) + + participant1.underlying.value.metrics.cryptoMetrics.kmsMetricsO shouldBe defined + + // Even when KMS is not used, signing and decryption operations always record latency metrics. + forAll(Seq(participant1, participant2)) { p => + p.underlying.value.metrics.cryptoMetrics.signingMetrics.signingLatency.valuesWithContext should not be empty + p.underlying.value.metrics.cryptoMetrics.decryptionMetrics.decryptLatency.valuesWithContext should not be empty + } + } + +} + +class GcpKmsCryptoMetricsIntegrationTestPostgres + extends CommunityIntegrationTest + with SharedEnvironment + with GcpKmsCryptoIntegrationTestBase + with KmsCryptoWithPreDefinedKeysIntegrationTest { + setupPlugins( + withAutoInit = false, + storagePlugin = Some(new UsePostgres(loggerFactory)), + sequencerPlugin = new UseBftSequencer(loggerFactory), + ) +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala index 65255c156b..3fa1ad0fb4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidAssignmentRequestIntegrationTest.scala @@ -64,7 +64,7 @@ final class InvalidAssignmentRequestIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala index 4ce90ff29d..5613504f67 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidReassignmentIdIntegrationTest.scala @@ -78,7 +78,7 @@ sealed trait InvalidReassignmentIdIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala index e73bdec3fb..2bd66734d3 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/InvalidUnassignmentRequestIntegrationTest.scala @@ -58,7 +58,7 @@ class InvalidUnassignmentRequestIntegrationTest EnvironmentDefinition.P3_S1M1_S1M1 .addConfigTransforms( ProgrammableSequencer.configOverride(getClass.toString, loggerFactory), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala index a8f1bd7c75..c5b0bfe60e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/KeyManagementIntegrationTest.scala @@ -22,7 +22,7 @@ import com.digitalasset.canton.util.OptionUtil import org.scalatest.Assertion trait KeyManagementIntegrationTestHelper extends KeyManagementTestHelper { - self: BaseIntegrationTest => + self: CantonBaseIntegrationTest => protected def waitForKeyTopologyUpdate( nodes: Seq[InstanceReference], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala index 0cdb6691f0..6ee91993a5 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoIntegrationTestBase.scala @@ -19,7 +19,7 @@ import com.digitalasset.canton.time.{RemoteClock, SimClock} * Check contributing/kms.md on how to run the tests */ trait KmsCryptoIntegrationTestBase extends TopologyManagementHelper { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => // Defines which nodes will run an external KMS. protected lazy val protectedNodes: Set[String] = Set("participant1") diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala index bce32988f0..9888b01cb4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/KmsCryptoWithPreDefinedKeysIntegrationTest.scala @@ -5,14 +5,14 @@ package com.digitalasset.canton.integration.tests.security.kms import com.digitalasset.canton.config.CryptoProvider import com.digitalasset.canton.crypto.store.KmsCryptoPrivateStore -import com.digitalasset.canton.integration.{CommunityIntegrationTest, EnvironmentSetup} +import com.digitalasset.canton.integration.{CantonEnvironmentSetup, CommunityIntegrationTest} /** Runs a crypto integration tests with one participant using a KMS provider with pre-generated * keys. Runs with persistence so we also check that it is able to recover from an unexpected * shutdown. */ trait KmsCryptoWithPreDefinedKeysIntegrationTest extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest & EnvironmentSetup => + self: CommunityIntegrationTest & CantonEnvironmentSetup => "be able to restart from a persisted state" in { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala index 3c4943d665..cbaf4b7052 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysIntegrationTest.scala @@ -38,7 +38,7 @@ trait SessionSigningKeysIntegrationTest override protected def otherConfigTransforms: Seq[ConfigTransform] = Seq( ConfigTransforms.setSigningKeysIfPV35OrHigher( - SessionSigningKeysConfig.default, + SessionSigningKeysConfig.enabled, nodeFilter = name => !nodesWithSessionSigningKeysDisabled.contains(name), ) ) @@ -48,13 +48,14 @@ trait SessionSigningKeysIntegrationTest ): Map[MetricsContext, Long] = { val kmsMetrics = node match { case p: LocalParticipantReference => - p.underlying.value.metrics.kmsMetrics + p.underlying.value.metrics.cryptoMetrics.kmsMetricsO.valueOrFail("no KMS metrics") case m: LocalMediatorReference => - m.underlying.value.replicaManager.mediatorRuntime.value.mediator.metrics.kmsMetrics + m.underlying.value.replicaManager.mediatorRuntime.value.mediator.metrics.cryptoMetrics.kmsMetricsO + .valueOrFail("no KMS metrics") case s: LocalSequencerReference => - s.underlying.value.sequencer.metrics.kmsMetrics + s.underlying.value.sequencer.metrics.cryptoMetrics.kmsMetricsO.valueOrFail("no KMS metrics") case _ => fail("unexpected node") } @@ -69,7 +70,7 @@ trait SessionSigningKeysIntegrationTest if (nodesWithSessionSigningKeysDisabled.contains(node.name)) node.config.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.disabled else - node.config.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.default + node.config.crypto.sessionSigningKeys shouldBe SessionSigningKeysConfig.enabled } assertPingSucceeds(participant1, participant2) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala index 65f9d3a139..cb652bc9b9 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/SessionSigningKeysLifecycleIntegrationTest.scala @@ -78,7 +78,10 @@ trait SessionSigningKeysLifecycleIntegrationTest // record initial metrics to establish a baseline, as a fallback to the long-term key // may have occurred during bootstrap. val participantKmsMetrics = Seq(participant1, participant2).map( - _.underlying.value.metrics.kmsMetrics.sessionSigningKeysFallback.valuesWithContext + _.underlying.value.metrics.cryptoMetrics.kmsMetricsO + .valueOrFail("no KMS metrics") + .sessionSigningKeysFallback + .valuesWithContext ) env.nodes.local.foreach { node => @@ -100,7 +103,10 @@ trait SessionSigningKeysLifecycleIntegrationTest // we expect that no fallback has been triggered for the ping requests and that // session signing keys have been used and rotated successfully. Seq(participant1, participant2).map( - _.underlying.value.metrics.kmsMetrics.sessionSigningKeysFallback.valuesWithContext + _.underlying.value.metrics.cryptoMetrics.kmsMetricsO + .valueOrFail("no KMS metrics") + .sessionSigningKeysFallback + .valuesWithContext ) shouldBe participantKmsMetrics } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala index da4569679d..64ec2190f4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsEncryptedCryptoPrivateStoreTestBase.scala @@ -5,13 +5,13 @@ package com.digitalasset.canton.integration.tests.security.kms.aws import com.digitalasset.canton.integration.plugins.UseAwsKms import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } trait AwsEncryptedCryptoPrivateStoreTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( protectedNodes: Set[String], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala index 12c653a22c..1e154dc6da 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/aws/AwsKmsCryptoIntegrationTestBase.scala @@ -16,14 +16,14 @@ import com.digitalasset.canton.integration.plugins.{ } import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, - EnvironmentSetup, EnvironmentSetupPlugin, } trait AwsKmsCryptoIntegrationTestBase extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected val kmsConfig: KmsConfig = KmsConfig.Aws.defaultTestConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala index d959f24c6a..a88b23e07d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpEncryptedCryptoPrivateStoreTestBase.scala @@ -6,13 +6,13 @@ package com.digitalasset.canton.integration.tests.security.kms.gcp import com.digitalasset.canton.config.KmsConfig import com.digitalasset.canton.integration.plugins.UseGcpKms import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } trait GcpEncryptedCryptoPrivateStoreTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( protectedNodes: Set[String], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala index fe03234d72..1f1d77e1e4 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/gcp/GcpKmsCryptoIntegrationTestBase.scala @@ -16,14 +16,14 @@ import com.digitalasset.canton.integration.plugins.{ } import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, ConfigTransforms, - EnvironmentSetup, EnvironmentSetupPlugin, } trait GcpKmsCryptoIntegrationTestBase extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected val kmsConfig: KmsConfig = KmsConfig.Gcp.defaultTestConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala index 6ce7858a09..495a93cf6d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockEncryptedCryptoPrivateStoreTestBase.scala @@ -6,13 +6,13 @@ package com.digitalasset.canton.integration.tests.security.kms.mock import com.digitalasset.canton.crypto.kms.mock.v1.MockKmsDriverFactory.mockKmsDriverName import com.digitalasset.canton.integration.plugins.UseKmsDriver import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } trait MockEncryptedCryptoPrivateStoreTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected def setupPlugins( protectedNodes: Set[String], diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala index 122cb1a701..fcdc56bfca 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/kms/mock/MockKmsDriverCryptoIntegrationTestBase.scala @@ -14,8 +14,8 @@ import com.digitalasset.canton.integration.plugins.{EncryptedPrivateStoreStatus, import com.digitalasset.canton.integration.tests.security.kms.KmsCryptoIntegrationTestBase import com.digitalasset.canton.integration.tests.security.kms.mock.MockKmsDriverCryptoIntegrationTestBase.mockKmsDriverConfig import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, - EnvironmentSetup, EnvironmentSetupPlugin, } import com.typesafe.config.ConfigValueFactory @@ -23,7 +23,7 @@ import com.typesafe.config.ConfigValueFactory import scala.jdk.CollectionConverters.* trait MockKmsDriverCryptoIntegrationTestBase extends KmsCryptoIntegrationTestBase { - self: CommunityIntegrationTest with EnvironmentSetup => + self: CommunityIntegrationTest with CantonEnvironmentSetup => protected val kmsConfig: KmsConfig = mockKmsDriverConfig diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala index 66f0a6b6d3..873d29794e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageRemovalIntegrationTest.scala @@ -73,7 +73,7 @@ sealed trait PackageRemovalIntegrationTest override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) // Note that CantonTests depends on CantonExamples private val cantonTestsPkg = PackageId.assertFromString(Many.PACKAGE_ID) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala index af2b8cccc9..6096935e97 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/security/pkgdars/PackageVettingIntegrationTest.scala @@ -47,6 +47,7 @@ import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.MaliciousParticipantNode import com.digitalasset.canton.util.ReassignmentTag.{Source, Target} import com.digitalasset.canton.util.ShowUtil.* +import com.digitalasset.canton.version.ProtocolVersion import com.digitalasset.daml.lf.archive.{DamlLf, DarParser, DarReader} import com.digitalasset.daml.lf.data.Ref.PackageId import monocle.macros.syntax.lens.* @@ -69,6 +70,8 @@ sealed trait PackageVettingIntegrationTest val ledgerIntegrity: SecurityTest = SecurityTest(property = Integrity, asset = "virtual shared ledger") + private val pvSupportsUnvettedDependencies: Boolean = testedProtocolVersion > ProtocolVersion.v34 + private lazy val pureCryptoRef: AtomicReference[CryptoPureApi] = new AtomicReference() def pureCrypto: CryptoPureApi = pureCryptoRef.get() @@ -84,7 +87,7 @@ sealed trait PackageVettingIntegrationTest _.focus(_.parameters.reassignmentsConfig.targetTimestampForwardTolerance) .replace(config.NonNegativeFiniteDuration.ofMinutes(10)) ), - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .withSetup { implicit env => import env.* @@ -591,7 +594,7 @@ sealed trait PackageVettingIntegrationTest } "all packages are stored, but a dependent package has not been vetted" must { - "refuse to vet the package" taggedAs_ { mit => + s"refuse to vet the package on PV=${ProtocolVersion.v34}" taggedAs_ { mit => ledgerIntegrity.setAttack( Attack( actor = "participant operator", @@ -599,7 +602,7 @@ sealed trait PackageVettingIntegrationTest mitigation = mit, ) ) - } in { implicit env => + } onlyRunWhen (!pvSupportsUnvettedDependencies) in { implicit env => import env.* participant3.packages.list().filter(_.packageId == packId) should not be empty @@ -620,9 +623,16 @@ sealed trait PackageVettingIntegrationTest ) in { implicit env => import env.* - // can vet dependencies one by one using force archive.dependencies.foreach { dep => - vettingCmd(adds = List(dep), force = ForceFlags(ForceFlag.AllowUnvettedDependencies)) + vettingCmd( + adds = List(dep), + force = + if (pvSupportsUnvettedDependencies) ForceFlags.none + else { + // can vet dependencies one by one using force + ForceFlags(ForceFlag.AllowUnvettedDependencies) + }, + ) eventually() { participant3.topology.vetted_packages .list(daId, filterParticipant = participant3.filterString) @@ -861,17 +871,27 @@ sealed trait PackageVettingIntegrationTest val vettingDepDar = tryReadDar(VettingDepPath) val vettingMainDar = tryReadDar(VettingMainPath) - "refuse to unvet if the package is used as a dependency" in { implicit env => - import env.* + s"refuse to unvet if the package is used as a dependency on PV=${ProtocolVersion.v34} or before" onlyRunWhen (!pvSupportsUnvettedDependencies) in { + implicit env => + import env.* - // upload and vet the main dar and its dependencies - participant3.dars.upload(VettingMainPath, vetAllPackages = true) + // upload and vet the main dar and its dependencies + participant3.dars.upload(VettingMainPath, vetAllPackages = true) - // unvetting the dep package should fail - assertThrowsAndLogsCommandFailures( - vettingCmd(removes = Seq(vettingDepDar.main)), - _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), - ) + assertThrowsAndLogsCommandFailures( + vettingCmd(removes = Seq(vettingDepDar.main)), + _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), + ) + } + + s"allow to unvet if the package is used as a dependency on PV=${ProtocolVersion.v35} or after" onlyRunWhen (pvSupportsUnvettedDependencies) in { + implicit env => + import env.* + + // upload and vet the main dar and its dependencies + participant3.dars.upload(VettingMainPath, vetAllPackages = true) + + vettingCmd(removes = Seq(vettingDepDar.main)) } "allow to unvet if the package is used as a dependency and AllowUnvettedDependencies is used" in { @@ -882,20 +902,35 @@ sealed trait PackageVettingIntegrationTest ) } - "refuse to unvet while vetting a dependent package" in { implicit env => - // vet the dep package and unvet the main package - vettingCmd( - adds = Seq(vettingDepDar.main), - removes = Seq(vettingMainDar.main), - ) + s"refuse to unvet while vetting a dependent package (PV=${ProtocolVersion.v34})" onlyRunWhen (!pvSupportsUnvettedDependencies) in { + implicit env => + // vet the dep package and unvet the main package + vettingCmd( + adds = Seq(vettingDepDar.main), + removes = Seq(vettingMainDar.main), + ) + + assertThrowsAndLogsCommandFailures( + vettingCmd( + adds = Seq(vettingMainDar.main), + removes = Seq(vettingDepDar.main), + ), + _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), + ) + } + + s"allow to unvet while vetting a dependent package (PV=${ProtocolVersion.v35}+)" onlyRunWhen pvSupportsUnvettedDependencies in { + implicit env => + // vet the dep package and unvet the main package + vettingCmd( + adds = Seq(vettingDepDar.main), + removes = Seq(vettingMainDar.main), + ) - assertThrowsAndLogsCommandFailures( vettingCmd( adds = Seq(vettingMainDar.main), removes = Seq(vettingDepDar.main), - ), - _.shouldBeCantonErrorCode(ParticipantTopologyManagerError.DependenciesNotVetted), - ) + ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala index a51722d849..bd393b5e28 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/DynamicOnboardingIntegrationTest.scala @@ -348,6 +348,17 @@ abstract class DynamicOnboardingIntegrationTest(val name: String) ) ) => message should include(s"was previously delivered at $aggregationSequenced2") + case SendResult.Error( + DeliverError( + _, + _, + _, + _, + SequencerErrors.AggregateSubmissionAlreadySentV2(message), + _, + ) + ) => + message should include(s"was previously delivered at $aggregationSequenced2") } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala index 27da1ddbe1..4ba7fb8e8a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/ToxiproxyIntegrationTest.scala @@ -8,6 +8,7 @@ import com.digitalasset.canton.concurrent.Threading import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.SequencerReference +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -83,7 +84,15 @@ abstract class ToxiproxyIntegrationTest // Use a threshold of two to ensure that the mediator connects to all sequencers. // TODO(#19911) Reduce to one again once this can be configured independently. Some( - testMediators.map(_ -> (testSequencers, PositiveInt.two, NonNegativeInt.zero)).toMap + testMediators + .map( + _ -> MediatorSequencersConfiguration( + testSequencers, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ) + ) + .toMap ), ) NetworkBootstrapper(Seq(description)) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala index 7b231f8657..25ba787e20 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftOrderingSequencerWithTrafficControlApiTestPostgres.scala @@ -17,6 +17,7 @@ import com.digitalasset.canton.synchronizer.sequencer.block.BlockSequencerFactor import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.bindings.canton.sequencing.BftSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.BftBlockOrdererConfig import com.digitalasset.canton.synchronizer.sequencer.config.SequencerNodeParameters +import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.synchronizer.sequencer.traffic.{ SequencerRateLimitManager, SequencerTrafficConfig, @@ -81,6 +82,7 @@ class BftOrderingSequencerWithTrafficControlApiTestPostgres synchronizerSyncCryptoApi: SynchronizerCryptoClient, protocolVersion: ProtocolVersion, trafficConfig: SequencerTrafficConfig, + lsuSequencingBounds: Option[LsuSequencingBounds], ): SequencerRateLimitManager = rateLimitManager } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala index 7792bde4ca..68d6f51cae 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/bftordering/BftSequencerApiTest.scala @@ -56,7 +56,9 @@ class BftSequencerApiTest extends SequencerApiTest with RateLimitManagerTesting asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq("MED", "PAR"), lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ) override final def createSequencer(crypto: SynchronizerCryptoClient)(implicit diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala index df6a325c54..c6cf3453a6 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerApiTest.scala @@ -91,7 +91,9 @@ class ReferenceSequencerApiTest extends SequencerApiTest with RateLimitManagerTe asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq("MED", "PAR"), lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ) "Reference sequencer" when runSequencerApiTests() diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala index 20c26d4f34..f3327bd1dc 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestBase.scala @@ -313,8 +313,10 @@ abstract class ReferenceSequencerWithTrafficControlApiTestBase asyncWriter = AsyncWriterParameters(), timeAdvancingTopology = TimeAdvancingTopologyConfig(), delayRequestsBeforeLsuTrafficInit = false, + enableRejectDeliveredAggregationsOnPv35 = Seq("MED"), disableSubmissionChecksForTesting = disableSubmissionChecksForTesting, lsuConfig = SequencerLsuConfig(), + enablePrevalidation = true, ) // Important to create the histograms before the factory, because creating the factory will // register them once and for all and we can't add more afterwards @@ -1320,6 +1322,7 @@ object ReferenceSequencerWithTrafficControlApiTestBase { trafficConfig, sequencerMemberRateLimiterFactory, eventCostCalculator, + lsuSequencingBounds = None, ) { private val isWriteSideEnforcementDisabled = new AtomicBoolean(false) private val readValidationResponse = new AtomicReference[ diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala index 7496aef193..fd36c5f241 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/sequencer/reference/ReferenceSequencerWithTrafficControlApiTestPostgres.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.synchronizer.metrics.SequencerMetrics import com.digitalasset.canton.synchronizer.sequencer.BlockSequencerConfig import com.digitalasset.canton.synchronizer.sequencer.block.DriverBlockSequencerFactory import com.digitalasset.canton.synchronizer.sequencer.config.SequencerNodeParameters +import com.digitalasset.canton.synchronizer.sequencer.time.LsuSequencingBounds import com.digitalasset.canton.synchronizer.sequencer.traffic.{ SequencerRateLimitManager, SequencerTrafficConfig, @@ -58,6 +59,7 @@ class ReferenceSequencerWithTrafficControlApiTestPostgres synchronizerSyncCryptoApi: SynchronizerCryptoClient, protocolVersion: ProtocolVersion, trafficConfig: SequencerTrafficConfig, + lsuSequencingBounds: Option[LsuSequencingBounds], ): SequencerRateLimitManager = rateLimitManager } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala index 6e48579537..4ac0db9363 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/PartyToParticipantAuthIntegrationTest.scala @@ -35,6 +35,7 @@ import org.scalatest.Assertion import org.slf4j.event.Level import java.util.UUID +import scala.concurrent.ExecutionContext trait PartyToParticipantAuthIntegrationTest extends CommunityIntegrationTest @@ -1096,7 +1097,10 @@ trait PartyToParticipantAuthIntegrationTest fingerprints: Seq[Fingerprint], serial: PositiveInt = PositiveInt.one, topologyChangeOp: TopologyChangeOp = TopologyChangeOp.Replace, - )(implicit env: FixtureParam): SignedTopologyTransaction[TopologyChangeOp, TopologyMapping] = { + )(implicit + env: FixtureParam, + ec: ExecutionContext, + ): SignedTopologyTransaction[TopologyChangeOp, TopologyMapping] = { val tx = topologyTransaction(topologyMapping, serial = serial, topologyChangeOp = topologyChangeOp) @@ -1128,13 +1132,13 @@ trait PartyToParticipantAuthIntegrationTest private def sign[Op <: TopologyChangeOp, M <: TopologyMapping]( topologyTransaction: TopologyTransaction[Op, M], fingerprint: Fingerprint, - )(implicit env: FixtureParam): Signature = + )(implicit env: FixtureParam, ec: ExecutionContext): Signature = signBytes(topologyTransaction.hash.hash.getCryptographicEvidence, fingerprint) private def signBytes( bytes: ByteString, fingerprint: Fingerprint, - )(implicit env: FixtureParam): Signature = + )(implicit env: FixtureParam, ec: ExecutionContext): Signature = env.tryGlobalCrypto.privateCrypto .signBytes( bytes, @@ -1148,7 +1152,7 @@ trait PartyToParticipantAuthIntegrationTest participant: ParticipantReference, partyId: PartyId, signingKey: SigningPublicKey, - )(implicit env: FixtureParam): ExecuteSubmissionAndWaitResponse = { + )(implicit env: FixtureParam, ec: ExecutionContext): ExecuteSubmissionAndWaitResponse = { val prepared: PrepareSubmissionResponse = participant.ledger_api.javaapi.interactive_submission.prepare( Seq(partyId), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala index d90f37df5a..fe80f4bd7a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/topology/TopologyValidationMultiSynchronizerIntegrationTest.scala @@ -36,7 +36,7 @@ class TopologyValidationMultiSynchronizerIntegrationTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 .addConfigTransforms( - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag ) .withSetup { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala index 10ceb0a2a3..d73949b73e 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/toxiproxy/slow/ToxiproxyBftSequencerConnectionsIntegrationTest.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.config import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} import com.digitalasset.canton.console.{CommandFailure, InstanceReference} import com.digitalasset.canton.error.TransactionRoutingError.TopologyErrors.UnknownContractSynchronizers +import com.digitalasset.canton.integration.bootstrap.NetworkTopologyDescription.MediatorSequencersConfiguration import com.digitalasset.canton.integration.bootstrap.{ NetworkBootstrapper, NetworkTopologyDescription, @@ -102,8 +103,11 @@ sealed trait ToxiproxyBftSequencerConnectionsIntegrationTest mediators = Seq(mediator1), overrideMediatorToSequencers = Some( Map( - mediator1 -> (sequencers.local, - /* trust threshold */ PositiveInt.two, /* liveness margin */ NonNegativeInt.zero) + mediator1 -> MediatorSequencersConfiguration( + sequencers.local, + trustThreshold = PositiveInt.two, + livenessMargin = NonNegativeInt.zero, + ) ) ), ) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala new file mode 100644 index 0000000000..99e19a1a6b --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/ParticipantTrafficEnforcementTest.scala @@ -0,0 +1,213 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.traffic + +import com.digitalasset.canton.config +import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.console.CommandFailure +import com.digitalasset.canton.integration.* +import com.digitalasset.canton.integration.util.{TestUtils, TrafficControlUtils} +import com.digitalasset.canton.ledger.error.CommonErrors.ServiceNotRunning +import com.digitalasset.canton.logging.LogEntry +import com.digitalasset.canton.platform.config.{ + TrafficEnforcementConfig, + TrafficEnforcementServerConfig, +} +import com.digitalasset.canton.topology.ExternalParty +import io.grpc.Status +import monocle.macros.syntax.lens.* +import org.scalatest.Assertion + +import java.time.Duration +import java.util.UUID +import scala.concurrent.duration.DurationInt + +sealed trait ParticipantTrafficEnforcementTest + extends CommunityIntegrationTest + with SharedEnvironment + with HasCycleUtils { + + protected var aliceE: ExternalParty = _ + + /** Participant config transforms to enable or disable participant-local traffic enforcement. + */ + protected def participantConfigTransforms: Seq[ConfigTransform] + + override def environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P2_S1M1 + .addConfigTransforms(participantConfigTransforms*) + .addConfigTransform(ConfigTransforms.enableInteractiveSubmissionTransforms) + .addConfigTransforms(ConfigTransforms.useStaticTime) + .addConfigTransform( + ConfigTransforms.updateAllSequencerConfigs_( + _.focus(_.trafficConfig.pruningRetentionWindow) + .replace(config.NonNegativeFiniteDuration.ofSeconds(5)) + .focus(_.trafficConfig.trafficPurchasedCacheSizePerMember) + .replace(PositiveInt.one) + ) + ) + .withSetup { implicit env => + import env.* + participants.local.foreach { participant => + participant.synchronizers.connect_local(sequencer1, alias = daName) + participant.dars.upload(CantonExamplesPath, synchronizerId = daId) + } + + aliceE = participant1.parties.testing.external.enable("Alice") + } + .withTrafficControl( + TestUtils.waitForTargetTimeOnSynchronizerNode(wallClock.now, logger), + trafficControlParameters = TrafficControlUtils.predictableTraffic, + topUpAllMembers = true, + disableCommitments = true, + ) + + protected def assertUnimplemented(entry: LogEntry): Assertion = + entry.message should include(Status.Code.UNIMPLEMENTED.toString) +} + +final class ParticipantTrafficEnforcementDisabledTest extends ParticipantTrafficEnforcementTest { + override protected def participantConfigTransforms: Seq[ConfigTransform] = Seq( + ConfigTransforms.updateParticipantConfig("participant1")( + _.focus(_.trafficEnforcement).replace(TrafficEnforcementConfig(enabled = false)) + ) + ) + + "Participant" when { + "traffic enforcement is disabled" should { + "not expose the traffic service endpoints on the Ledger API" in { implicit env => + import env.* + + assertThrowsAndLogsCommandFailures( + participant1.ledger_api.traffic.get_account(aliceE.partyId.toProtoPrimitive), + assertUnimplemented, + ) + + assertThrowsAndLogsCommandFailures( + participant1.ledger_api.traffic.update_account( + aliceE.partyId.toProtoPrimitive, + None, + ), + assertUnimplemented, + ) + } + } + + "support interactive submissions" in { implicit env => + import env.* + + // Pass some time to allow traffic re-fill the submission below + environment.simClock.value.advance(Duration.ofSeconds(5L)) + + // Prepare and execute should work seamlessly + val prepared = participant1.ledger_api.interactive_submission.prepare( + actAs = Seq(aliceE), + commands = Seq(createCycleCommand(aliceE.partyId, "traffic")), + hashingSchemeVersion = testedApiHashingSchemeVersion, + ) + + participant1.ledger_api.interactive_submission.execute_and_wait( + prepared.getPreparedTransaction, + Map(aliceE.partyId -> global_secret.sign(prepared.preparedTransactionHash, aliceE)), + UUID.randomUUID().toString, + prepared.hashingSchemeVersion, + ) + } + } +} + +final class ParticipantTrafficEnforcementEnabledTest extends ParticipantTrafficEnforcementTest { + private val nonExistentTeaServerName = s"non-existent-tea-server" + + override protected def participantConfigTransforms: Seq[ConfigTransform] = Seq( + ConfigTransforms.updateParticipantConfig("participant1")( + _.focus(_.trafficEnforcement) + .replace( + TrafficEnforcementConfig( + enabled = true, + trafficEnforcementServer = + TrafficEnforcementServerConfig.Internal(nonExistentTeaServerName), + ) + ) + ), + // Shorten network timeout so retries to the non-existent traffic service give up quickly + _.focus(_.parameters.timeouts.processing.network) + .replace(config.NonNegativeDuration.tryFromDuration(5.seconds)), + ) + + "Participant" when { + "traffic enforcement is enabled but traffic enforcement server is not available" should { + "return graceful errors on traffic and interactive submission service endpoints" in { + implicit env => + import env.* + + def assertEntriesTeaUnavailable(entries: Seq[LogEntry]): Assertion = + entries.foldLeft(succeed) { case (_, entry) => + entry.message should ((include(ServiceNotRunning.id) and include( + "User traffic service is not running" + )) or + (include(Status.Code.UNAVAILABLE.toString) and include( + s"Could not find server: $nonExistentTeaServerName" + ) or + include("Retry timeout has elapsed, giving up."))) + } + + // GetAccount on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.traffic.get_account(aliceE.partyId.toProtoPrimitive), + assertEntriesTeaUnavailable, + ) + + // UpdateAccount on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.traffic.update_account( + aliceE.partyId.toProtoPrimitive, + None, + ), + assertEntriesTeaUnavailable, + ) + + // Preparing on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.interactive_submission.prepare( + actAs = Seq(aliceE), + commands = Seq(createCycleCommand(aliceE.partyId, "traffic")), + hashingSchemeVersion = testedApiHashingSchemeVersion, + ), + assertEntriesTeaUnavailable, + ) + + // Prepare a transaction on P2 for Alice (P2 does not have traffic enabled so we can prepare) + val prepared = participant2.ledger_api.interactive_submission.prepare( + actAs = Seq(aliceE), + commands = Seq(createCycleCommand(aliceE.partyId, "traffic")), + hashingSchemeVersion = testedApiHashingSchemeVersion, + ) + + // Executing on P1 fails due to TEA not enabled + loggerFactory.assertThrowsAndLogsSeq[CommandFailure]( + participant1.ledger_api.interactive_submission.execute_and_wait( + prepared.getPreparedTransaction, + Map(aliceE.partyId -> global_secret.sign(prepared.preparedTransactionHash, aliceE)), + UUID.randomUUID().toString, + prepared.hashingSchemeVersion, + // Short timeout to reduce test time + optTimeout = Some(5.seconds), + ), + _.foldLeft(succeed) { case (_, entry) => + entry.message should ((include(ServiceNotRunning.id) and include( + "User traffic service is not running" + )) or + (include(Status.Code.UNAVAILABLE.toString) and include( + s"Could not find server: $nonExistentTeaServerName" + )) or + include("Retry timeout has elapsed, giving up.") or + include("Failed to submit submission") or + include("DEADLINE_EXCEEDED")) + }, + ) + } + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala index e78ba7250c..113b1abb73 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/traffic/TrafficControlTest.scala @@ -11,6 +11,7 @@ import com.digitalasset.canton.admin.api.client.data.{ ComponentHealthState, TrafficControlParameters, } +import com.digitalasset.canton.annotations.UnstableTest import com.digitalasset.canton.config.CantonRequireTypes.InstanceName import com.digitalasset.canton.config.RequireTypes.{ NonNegativeLong, @@ -1006,6 +1007,7 @@ trait TrafficControlTest ) } +@UnstableTest // TOOD(#31976) class TrafficControlTestBftOrderingPostgres extends TrafficControlTest { private val useBftSequencer = new UseBftSequencer( loggerFactory, @@ -1018,6 +1020,7 @@ class TrafficControlTestBftOrderingPostgres extends TrafficControlTest { registerPlugin(new UseProgrammableSequencer(this.getClass.toString, loggerFactory)) } +@UnstableTest // TOOD(#32073) class TrafficControlTestBftOrderingH2 extends TrafficControlTest { private val useBftSequencer = new UseBftSequencer( loggerFactory, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala index 49190e2964..9de116b926 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/CantonNetworkTopologyStateIntegrationTest.scala @@ -12,9 +12,9 @@ import com.digitalasset.canton.integration.plugins.toxiproxy.UseToxiproxy.Toxipr import com.digitalasset.canton.integration.plugins.toxiproxy.{ParticipantToPostgres, UseToxiproxy} import com.digitalasset.canton.integration.tests.toxiproxy.ToxiproxyHelpers import com.digitalasset.canton.integration.{ + CantonEnvironmentSetup, CommunityIntegrationTest, EnvironmentDefinition, - EnvironmentSetup, SharedEnvironment, TestConsoleEnvironment, } @@ -43,7 +43,7 @@ import scala.annotation.nowarn import scala.concurrent.duration.* trait CantonNetworkTopologyIntegrationTestBase extends CommunityIntegrationTest { - this: EnvironmentSetup => + this: CantonEnvironmentSetup => protected def runValidation( topoStoreIdx: Int, txs: GenericStoredTopologyTransactions, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala index ed12c111e3..fdf2b997c6 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuCommandIdIntegrationTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.integration.tests.upgrade.lsu import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.MediatorError +import com.digitalasset.canton.error.TransactionRoutingError.ConfigurationErrors import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UseProgrammableSequencer} @@ -13,15 +14,11 @@ import com.digitalasset.canton.integration.tests.examples.IouSyntax import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer import com.digitalasset.canton.integration.{EnvironmentDefinition, TestEnvironment} import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors.DuplicateCommand -import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality.{ - Optional, - OptionalMany, -} import com.digitalasset.canton.participant.protocol.TransactionProcessor.SubmissionErrors import com.digitalasset.canton.participant.protocol.TransactionProcessor.SubmissionErrors.SequencerRequest +import com.digitalasset.canton.participant.sync.SyncServiceInjectionError import com.digitalasset.canton.protocol.LocalRejectError.TimeRejects -import com.digitalasset.canton.sequencing.protocol.SubmissionRequest -import com.digitalasset.canton.synchronizer.sequencer.errors.SequencerError +import com.digitalasset.canton.sequencing.protocol.{SequencerErrors, SubmissionRequest} import com.digitalasset.canton.synchronizer.sequencer.{HasProgrammableSequencer, SendDecision} import com.digitalasset.canton.topology.PartyId import com.google.rpc.Code @@ -145,48 +142,35 @@ final class LsuCommandIdIntegrationTest extends LsuBase with HasProgrammableSequ val cmdIdAtUpgradeTime = "cmd-id-at-upgrade-time" withClue("test command at upgrade time") { - loggerFactory.assertLogsUnorderedOptional( - { - // Move to upgrade time so that command submissions will fail due to overlap with LSU - environment.simClock.value.advanceTo(upgradeTime) - - val offsetBeforeSubmit = participant1.ledger_api.state.end() - participant1.ledger_api.javaapi.commands - .submit_async(Seq(bank), createIouCmd, commandId = cmdIdAtUpgradeTime) - - // Move forward until the new psid is up - environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) - transferTraffic(suppressLogs = false) - eventually() { - environment.simClock.value.advance(Duration.ofSeconds(1)) - participants.all.forall(_.synchronizers.is_connected(fixture.newPsid)) shouldBe true - } - waitForTargetTimeOnSequencer(sequencer2, environment.clock.now, logger) + // Move to upgrade time so that command submissions will fail due to overlap with LSU + environment.simClock.value.advanceTo(upgradeTime) + // wait for the mediator to observe the upgrade time on the old synchronizer. + mediator1.testing.await_synchronizer_time(upgradeTime, commandTimeouts.ledgerCommand) - oldSynchronizerNodes.all.stop() + assertThrowsAndLogsCommandFailures( + participant1.ledger_api.javaapi.commands + .submit(Seq(bank), createIouCmd, commandId = cmdIdAtUpgradeTime), + _.message should ( + include(SyncServiceInjectionError.NotConnectedToAnySynchronizer.id) or + include(ConfigurationErrors.SubmissionSynchronizerNotReady.id) or + include(SubmissionErrors.TimeoutError.id) or + include(SequencerErrors.PassedUpgradeTime.id) or + // the command gets rejected with this error, when the synthetic LSU tombstone gets sequenced and processed + // before the command gets submitted, and therefore it gets rejected directly during the synchronous processing of sendAsync + include(SequencerRequest.id) + ), + ) - // Move further forward until this decision timeout has expired, and expect to see our submission timeout on the completion stream - environment.simClock.value.advance(decisionTimeout.plusSeconds(1).asJava) - participant1.health.ping(participant1) // To notify the sequencer that time has passed + // Move forward until the new psid is up + environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) + transferTraffic(suppressLogs = false) + eventually() { + environment.simClock.value.advance(Duration.ofSeconds(1)) + participants.all.forall(_.synchronizers.is_connected(fixture.newPsid)) shouldBe true + } + waitForTargetTimeOnSequencer(sequencer2, environment.clock.now, logger) - assertCommandEventuallyFailed( - cmdIdAtUpgradeTime, - offsetBeforeSubmit, - bank, - _ should ( - include(TimeRejects.LocalTimeout.id) or - include(SubmissionErrors.TimeoutError.id) or - // the command gets rejected with this error, when the synthetic LSU tombstone gets sequenced and processed - // before the command gets submitted, and therefore it gets rejected right at the start - include(SequencerRequest.id) - ), - ) - }, - Optional -> (_.warningMessage should (include regex "Response message for request .* timed out at")), - Optional -> (_.warningMessage should include("Submission timed out at")), - Optional -> (_.warningMessage should include("Time validation has failed")), - OptionalMany -> (_.shouldBeCantonErrorCode(SequencerError.NotAtUpgradeTimeOrBeyond)), - ) + oldSynchronizerNodes.all.stop() } withClue( @@ -211,7 +195,7 @@ final class LsuCommandIdIntegrationTest extends LsuBase with HasProgrammableSequ sinceOffset: Long, party: PartyId, assertErrorMessage: String => Unit, - )(implicit env: TestEnvironment): Unit = { + )(implicit env: TestEnvironment[?]): Unit = { val status = env.participant1.ledger_api.completions .list( party, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala index ab0cc39b2c..036920bb83 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEarlyHandshakeIntegrationTest.scala @@ -186,7 +186,7 @@ final class LsuEarlyHandshakeIntegrationTest extends LsuBase { forAll(participants.local)( _.topology.lsu.sequencer_successors .list() - .filter(_.item.sequencerId == sequencer1.id) + .filter(_.item.sequencerId == sequencer2.id) .loneElement ) } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala index f57a58660a..309ef2387a 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuEndToEndIntegrationTest.scala @@ -3,6 +3,16 @@ package com.digitalasset.canton.integration.tests.upgrade.lsu +import com.daml.nonempty.NonEmpty +import com.digitalasset.canton.SequencerAlias +import com.digitalasset.canton.admin.api.client.data.RegisteredSynchronizer.Status +import com.digitalasset.canton.admin.api.client.data.{ + KnownPhysicalSynchronizerId, + RegisteredSynchronizer, + SequencerConnection, + SynchronizerConnectionConfig, + SynchronizerPredecessor, +} import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.* import com.digitalasset.canton.integration.* @@ -11,7 +21,10 @@ import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} import com.digitalasset.canton.integration.tests.examples.IouSyntax +import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.Fixture +import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuEndToEndIntegrationTest.ExpectedRegisteredSynchronizers import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer +import monocle.macros.syntax.lens.* import java.time.Duration import scala.jdk.CollectionConverters.* @@ -64,6 +77,25 @@ sealed trait LsuEndToEndIntegrationTest extends LsuBase { val fixture = fixtureWithDefaults() performSynchronizerNodesLsu(fixture) + val expectedRegisteredSynchronizers = clue("Values returned by list registered before LSU") { + val (currentConfig, currentConfiguredPsid, _) = + participant1.synchronizers.list_registered().loneElement + + val expectedRegisteredSynchronizers = + ExpectedRegisteredSynchronizers(currentConfig, fixture) + + currentConfiguredPsid.toOption.value shouldBe fixture.currentPsid + + eventually() { + participant1.synchronizers.list_all_registered() should contain theSameElementsAs Seq( + expectedRegisteredSynchronizers.currentRegisteredSynchronizerBeforeLsu, + expectedRegisteredSynchronizers.newRegisteredSynchronizerBeforeLsu, + ) + } + + expectedRegisteredSynchronizers + } + environment.simClock.foreach(_.advanceTo(upgradeTime.immediateSuccessor)) transferTraffic() @@ -73,6 +105,22 @@ sealed trait LsuEndToEndIntegrationTest extends LsuBase { participants.all.forall(_.synchronizers.is_connected(fixture.currentPsid)) shouldBe false } + clue("Values returned by list registered after LSU") { + val (_, currentConfiguredPsid, _) = + participant1.synchronizers.list_registered().loneElement + + currentConfiguredPsid.toOption.value shouldBe fixture.newPsid + + eventually() { + participant1.synchronizers.list_all_registered() should contain theSameElementsAs Seq( + expectedRegisteredSynchronizers.currentRegisteredSynchronizerBeforeLsu + .copy(isConnected = false, status = Status.LsuSource), + expectedRegisteredSynchronizers.newRegisteredSynchronizerBeforeLsu + .copy(isConnected = true, status = Status.Active), + ) + } + } + oldSynchronizerNodes.all.stop() waitForTargetTimeOnSequencer(sequencer2, upgradeTime.immediateSuccessor, logger) @@ -116,6 +164,52 @@ sealed trait LsuEndToEndIntegrationTest extends LsuBase { } } +private object LsuEndToEndIntegrationTest { + import org.scalatest.OptionValues.* + + final case class ExpectedRegisteredSynchronizers( + initialSynchronizerConnectionConfig: SynchronizerConnectionConfig, + fixture: Fixture, + )(implicit env: TestConsoleEnvironment) { + import env.* + + val currentRegisteredSynchronizerBeforeLsu = RegisteredSynchronizer( + config = initialSynchronizerConnectionConfig, + status = Status.Active, + psid = KnownPhysicalSynchronizerId(fixture.currentPsid), + predecessor = None, + isConnected = true, + ) + + val newSequencerConnections: NonEmpty[Map[SequencerAlias, SequencerConnection]] = NonEmpty + .from( + Map( + sequencer1.sequencerAlias -> sequencer2.sequencerConnection + .copy(sequencerAlias = sequencer1.sequencerAlias, sequencerId = Some(sequencer1.id)) + ) + ) + .value + + val newRegisteredSynchronizerBeforeLsu = RegisteredSynchronizer( + config = initialSynchronizerConnectionConfig + .focus(_.sequencerConnections.aliasToConnection) + .replace(newSequencerConnections) + .focus(_.synchronizerId) + .replace(Some(fixture.newPsid)), + status = Status.LsuTarget, + psid = KnownPhysicalSynchronizerId(fixture.newPsid), + predecessor = Some( + SynchronizerPredecessor( + psid = fixture.currentPsid, + upgradeTime = fixture.upgradeTime, + isLateUpgrade = false, + ) + ), + isConnected = false, + ) + } +} + final class LsuEndToEndSimClockIntegrationTest extends LsuEndToEndIntegrationTest final class LsuEndToEndWallClockIntegrationTest extends LsuEndToEndIntegrationTest { diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala new file mode 100644 index 0000000000..4b611224d4 --- /dev/null +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuExhaustiveHandshakeIntegrationTest.scala @@ -0,0 +1,206 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.integration.tests.upgrade.lsu + +import com.daml.metrics.api.MetricQualification +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.integration.* +import com.digitalasset.canton.integration.EnvironmentDefinition.S2M2 +import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper +import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer +import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} +import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.{ + getLsuStatusMetricValues, + getParticipantHandshakesMetricValues, +} +import com.digitalasset.canton.logging.SuppressionRule +import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} +import com.digitalasset.canton.participant.metrics.ParticipantMetrics +import com.digitalasset.canton.participant.synchronizer.grpc.GrpcSynchronizerRegistry +import com.digitalasset.canton.{UniquePortGenerator, config} +import monocle.macros.syntax.lens.* +import org.slf4j.event.Level + +/** The goal of this test is to ensure that participants do the handshake with as many sequencers as + * possible and that this "waiting" is interrupted in case of shutdown or when minimumDuration has + * elapsed. + * + * Topology: + * - P1, P2, P3 connected to S1 and S2 with threshold=1 + * - The handshake is done as soon as S1 successor is known + * - Announcement of S2 successor triggers another handshake on which we do checks/assertions + * + * - Two sequencers: + * - S1 has S3 as successor + * - S2 has S4 as successor + * + * Tests: + * - P1 waits until all sequencers are up + * - P2 stops waiting because of shutdown + * - P3 stops waiting because of minimumDuration has elapsed + */ +final class LsuExhaustiveHandshakeIntegrationTest extends LsuBase { + + override protected def testName: String = "lsu-exhaustive-handshake" + + registerPlugin( + new UseBftSequencer( + loggerFactory, + MultiSynchronizer.tryCreate(Set("sequencer1", "sequencer2"), Set("sequencer3", "sequencer4")), + ) + ) + registerPlugin(new UsePostgres(loggerFactory)) + + override protected lazy val newOldSequencers: Map[String, String] = + Map("sequencer3" -> "sequencer1", "sequencer4" -> "sequencer2") + override protected lazy val newOldMediators: Map[String, String] = + Map("mediator3" -> "mediator1", "mediator4" -> "mediator2") + override protected lazy val upgradeTime: CantonTimestamp = CantonTimestamp.Epoch.plusSeconds(30) + + override lazy val environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition.P3S4M4_Config + .withNetworkBootstrap { implicit env => + new NetworkBootstrapper(S2M2) + } + .addConfigTransforms( + _.focus(_.monitoring.metrics) + .replace( + MetricsConfig( + qualifiers = Seq[MetricQualification](MetricQualification.Debug), + reporters = Seq( + MetricsReporterConfig.Prometheus( + port = UniquePortGenerator.next + ) + ), + ) + ) + ) + .addConfigTransform( + ConfigTransforms.updateAllParticipantConfigs { + + case ("participant1" | "participant2", pConfig) => + pConfig + .focus(_.parameters.lsu.handshake.minimumDuration) + // big value to ensure that the minimumDuration does not interrupt the wait + .replace(Some(config.NonNegativeFiniteDuration.ofDays(1))) + + case ("participant3", pConfig) => + pConfig + .focus(_.parameters.lsu.handshake.minimumDuration) + // small value to ensure that the minimumDuration interrupts the wait + .replace(Some(config.NonNegativeFiniteDuration.ofSeconds(3))) + + case (_other, pConfig) => pConfig + } + ) + .addConfigTransforms(configTransforms*) + .withSetup { implicit env => + import env.* + + defaultEnvironmentSetup(connectParticipants = false) + + participants.local.foreach( + _.synchronizers.connect_by_config( + synchronizerConnectionConfig(Seq(sequencer1, sequencer2), threshold = 1) + ) + ) + } + + "Participants" should { + "perform the handshake with as many sequencers as possible" in { implicit env => + import env.* + + val fixture = fixtureWithDefaults() + val p2Id = participant2.id + + fixture.oldSynchronizerOwners.foreach( + _.topology.lsu.announcement.propose(fixture.newPsid, fixture.upgradeTime) + ) + + // Ensure all nodes see the announcement + eventually() { + forAll(fixture.oldSynchronizerNodes.all ++ participants.local)( + _.topology.lsu.announcement + .list(store = Some(fixture.currentPsid)) + .filter(_.item.successorSynchronizerId == fixture.newPsid) + .loneElement + ) + } + + migrateSynchronizerNodes(fixture) + + /* + Stopping S4 so that handshake with S4 fails. + Stopping the mediators first to avoid connectivity warnings in the logs. + */ + + // Ensure that handshake with S4 fails + fixture.newSynchronizerNodes.mediators.stop() + sequencer4.stop() + + sequencer1.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer1.id, + endpoints = sequencer3.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ) + + // Initial handshake succeed because threshold=1 + eventually() { + forAll(participants.local) { p => + getLsuStatusMetricValues(p) + .get(fixture.newPsid) + .value should be >= ParticipantMetrics.LsuStatus.LocalCopyDone + } + } + + loggerFactory.assertEventuallyLogsSeq( + SuppressionRule.Level(Level.DEBUG) && SuppressionRule.forLogger[GrpcSynchronizerRegistry] + )( + sequencer2.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer2.id, + endpoints = sequencer4.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ), + entries => { + // All participants start the waiting + forExactly(3, entries)( + _.debugMessage should include("Handshake was successful. Starting to wait until") + ) + // P3 eventually stops waiting + forExactly(1, entries) { entry => + entry.debugMessage should include( + "Stopping the wait because max waiting time is reached." + ) + entry.loggerName should include("participant3") + } + }, + ) + + loggerFactory.assertLogsSeq( + SuppressionRule.Level(Level.DEBUG) && SuppressionRule.forLogger[GrpcSynchronizerRegistry] + )( + participant2.stop(), + forExactly(1, _) { entry => + entry.debugMessage should include("Stopping the wait because of shutdown.") + entry.loggerName should include("participant2") + }, + ) + + // Starting S4 should eventually allow P1 to handshake with S4 + sequencer4.start() + + eventually() { + getParticipantHandshakesMetricValues(sequencer4) + .get((participant1.id, "success")) + .value should be >= 1L + } + + getParticipantHandshakesMetricValues(sequencer4).get((p2Id, "success")) shouldBe empty + + getParticipantHandshakesMetricValues(sequencer4).get( + (participant3.id, "success") + ) shouldBe empty + } + } +} diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala index 7b9f6d89ef..86038a4033 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuIncorrectSequencerIdentityIntegrationTest.scala @@ -135,6 +135,20 @@ final class LsuIncorrectSequencerIdentityIntegrationTest extends LsuBase { LogEntryOptionality.OptionalMany, _.shouldBeCantonErrorCode(SequencerError.NotAtUpgradeTimeOrBeyond), ), + // sequencer1 fails to contact/handshake its successor + ( + LogEntryOptionality.Required, + _.warningMessage should include( + s"Error when contacting successor: expecting sequencer id to be ${sequencer1.id} but found ${sequencer2.id}" + ), + ), + // sequencer2 fails to contact/handshake its successor + ( + LogEntryOptionality.Required, + _.warningMessage should include( + s"Error when contacting successor: expecting sequencer id to be ${sequencer2.id} but found ${sequencer1.id}" + ), + ), ) } } @@ -288,6 +302,13 @@ final class LsuSuccessorSequencerIsPredecessorIntegrationTest extends LsuBase { LogEntryOptionality.OptionalMany, _.shouldBeCantonErrorCode(SequencerError.NotAtUpgradeTimeOrBeyond), ), + // failed contact between sequencers and their successor + ( + LogEntryOptionality.OptionalMany, + _.warningMessage should include( + s"Error when contacting successor: expecting psid to be ${fixture.newPsid} but found ${fixture.currentPsid}" + ), + ), ) } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala index 0f1a42ada0..6ebaff3b89 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuMetricsIntegrationTest.scala @@ -85,7 +85,7 @@ final class LsuMetricsIntegrationTest extends LsuBase { .addConfigTransform( // We want to retry more aggressively in the test ConfigTransforms.updateAllParticipantConfigs_( - _.focus(_.parameters.lsu.handshakeRetry) + _.focus(_.parameters.lsu.handshake.retry) .replace( ExponentialBackoffConfig( initialDelay = config.NonNegativeFiniteDuration.ofMillis(100), diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala index 46a58c9120..dc6a7f8b4b 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuOfflinePartyReplicationIntegrationTest.scala @@ -100,7 +100,7 @@ abstract class LsuOfflinePartyReplicationIntegrationTest extends LsuBase with Ha protected val acsSnapshotFile: TempFile = tempDirectory.toTempFile("offpr_test_acs_snapshot.gz") - protected def makeFixture1(implicit env: TestEnvironment): Fixture = Fixture( + protected def makeFixture1(implicit env: TestEnvironment[?]): Fixture = Fixture( currentPsid = env.daId, upgradeTime = upgradeTime1, oldSynchronizerNodes = SynchronizerNodes(Seq(env.sequencer1), Seq(env.mediator1)), @@ -111,16 +111,17 @@ abstract class LsuOfflinePartyReplicationIntegrationTest extends LsuBase with Ha newSerial = env.daId.serial.increment.toNonNegative, ) - protected def makeFixture2(fixture1: Fixture)(implicit env: TestEnvironment): Fixture = Fixture( - currentPsid = fixture1.newPsid, - upgradeTime = upgradeTime2, - oldSynchronizerNodes = fixture1.newSynchronizerNodes, - newSynchronizerNodes = SynchronizerNodes(Seq(env.sequencer3), Seq(env.mediator3)), - newOldNodesResolution = Map("sequencer3" -> "sequencer2", "mediator3" -> "mediator2"), - oldSynchronizerOwners = Set[InstanceReference](env.sequencer2, env.mediator2), - newPV = testedProtocolVersion, - newSerial = fixture1.newSerial.increment.toNonNegative, - ) + protected def makeFixture2(fixture1: Fixture)(implicit env: TestEnvironment[?]): Fixture = + Fixture( + currentPsid = fixture1.newPsid, + upgradeTime = upgradeTime2, + oldSynchronizerNodes = fixture1.newSynchronizerNodes, + newSynchronizerNodes = SynchronizerNodes(Seq(env.sequencer3), Seq(env.mediator3)), + newOldNodesResolution = Map("sequencer3" -> "sequencer2", "mediator3" -> "mediator2"), + oldSynchronizerOwners = Set[InstanceReference](env.sequencer2, env.mediator2), + newPV = testedProtocolVersion, + newSerial = fixture1.newSerial.increment.toNonNegative, + ) protected def assertParticipantHostsParty( participant: ParticipantReference, diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala index 8310dece36..208cbbe94c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuPurgeStoresAfterLsuIntegrationTest.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.integration.tests.examples.IouSyntax import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer import com.digitalasset.canton.integration.{ConfigTransforms, EnvironmentDefinition} import com.digitalasset.canton.participant.config.PurgeConfig +import com.digitalasset.canton.topology.transaction.TopologyMapping import monocle.macros.syntax.lens.* import java.time.Duration @@ -62,6 +63,7 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { PurgeConfig().copy( chunkSize = PositiveInt.tryCreate(2), cron = "/5 * * * * ?", + purgeableStoresListValidity = config.NonNegativeFiniteDuration.ofSeconds(1), ) ) ) @@ -85,8 +87,9 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { performSynchronizerNodesLsu(fixture) - clue("old stores are not empty") { - oldTopologyStore.dumpStoreContent().futureValueUS.result should not be empty + val oldTopology = clue("old stores are not empty") { + val oldTopology = oldTopologyStore.dumpStoreContent().futureValueUS.result.toSet + oldTopology should not be empty participant1.underlying.value.sync.syncPersistentStateManager .get(fixture.currentPsid) @@ -94,6 +97,8 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { .submissionTrackerStore .size .futureValueUS shouldBe 1 + + oldTopology } environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) @@ -126,8 +131,16 @@ final class LsuPurgeStoresAfterLsuIntegrationTest extends LsuBase { .value .topologyStore - clue("new stores are not empty") { - newTopologyStore.dumpStoreContent().futureValueUS.result should not be empty + clue("new stores are not purged") { + newTopologyStore + .dumpStoreContent() + .futureValueUS + .result + .toSet shouldBe oldTopology + // LsuSequencerConnectionSuccessor is filtered out when doing the local copy + .filterNot( + _.mapping.code == TopologyMapping.Code.LsuSequencerConnectionSuccessor + ) participant1.underlying.value.sync.syncPersistentStateManager .get(fixture.newPsid) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala index a5e34e150a..7b970120e8 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuReassignmentsIntegrationTest.scala @@ -52,7 +52,7 @@ final class LsuReassignmentsIntegrationTest extends LsuBase { NetworkBootstrapper(S1M1_S1M1) } .addConfigTransforms(configTransforms*) - .addConfigTransform(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransform(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => defaultEnvironmentSetup() } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala index 6ee7f6a414..751f21c4a7 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSanityCheckSuccessorSynchronizerIntegrationTest.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.integration.plugins.{ UseReferenceBlockSequencer, } import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.getLsuSequencingTestMetricValues +import com.digitalasset.canton.logging.SuppressingLogger.LogEntryOptionality import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} import com.digitalasset.canton.synchronizer.sequencer.block.bftordering.core.modules.output.OutputModule @@ -101,21 +102,45 @@ sealed abstract class LsuSanityCheckSuccessorSynchronizerIntegrationTest extends m.get(sequencer3.id).value should be > 0L } - sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) - sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) - eventually() { - getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 2 - getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 2 - } - - // Check whether the command behaves well with a restart - sequencer4.stop() - sequencer4.start() - sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) - eventually() { - getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 3 - getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 3 - } + mediator3.stop() + mediator3.start() + + loggerFactory.assertLogsUnorderedOptional( + { + sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) + sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) + eventually() { + getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 2 + getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 2 + } + + // Check whether the command behaves well with a restart + sequencer4.stop() + sequencer4.start() + sequencer4.setup.test_lsu_sequencing(NonNegativeInt.zero) + eventually() { + getLsuSequencingTestMetricValues(mediator3).get(sequencer4.id).value shouldBe 3 + getLsuSequencingTestMetricValues(mediator4).get(sequencer4.id).value shouldBe 3 + } + }, + // can happen if one mediator tries to ack when sequencer4 is stopped + ( + LogEntryOptionality.OptionalMany, + _.warningMessage should include("Failed to acknowledge clean timestamp"), + ), + ( + LogEntryOptionality.OptionalMany, + _.warningMessage should include( + "Is the server running? Did you configure the server address" + ), + ), + ( + LogEntryOptionality.Optional, + _.warningMessage should include( + "shutdown did not complete gracefully in allotted 3 seconds" + ), + ), + ) environment.simClock.value.advanceTo(upgradeTime.immediateSuccessor) diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala index 8426e7fbe5..04a5e2805d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSequencerContactSuccessorIntegrationTest.scala @@ -7,7 +7,7 @@ import com.daml.metrics.api.MetricQualification import com.digitalasset.canton.UniquePortGenerator import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.integration.* -import com.digitalasset.canton.integration.EnvironmentDefinition.S1M1 +import com.digitalasset.canton.integration.EnvironmentDefinition.S2M2 import com.digitalasset.canton.integration.bootstrap.NetworkBootstrapper import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer import com.digitalasset.canton.integration.plugins.{UseBftSequencer, UsePostgres} @@ -16,11 +16,16 @@ import com.digitalasset.canton.integration.tests.upgrade.lsu.LsuBase.{ getLsuSuccessorContactStatusMetricValues, } import com.digitalasset.canton.integration.util.TestUtils.waitForTargetTimeOnSequencer +import com.digitalasset.canton.logging.{LogEntry, SuppressionRule} import com.digitalasset.canton.metrics.{MetricsConfig, MetricsReporterConfig} import monocle.macros.syntax.lens.* +import org.slf4j.event.Level import java.time.Duration +/** Each sequencer attempts to contact its successor when it processes its own successor + * announcement. Upon successful contact, a metric should be changed. + */ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { override protected def testName: String = "lsu_sequencer_contact_successor" @@ -28,23 +33,27 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { registerPlugin( new UseBftSequencer( loggerFactory, - MultiSynchronizer.tryCreate(Set("sequencer1"), Set("sequencer2")), + MultiSynchronizer.tryCreate(Set("sequencer1", "sequencer2"), Set("sequencer3", "sequencer4")), ) ) registerPlugin(new UsePostgres(loggerFactory)) override protected lazy val newOldSequencers: Map[String, String] = Map( - "sequencer2" -> "sequencer1" + "sequencer3" -> "sequencer1", + "sequencer4" -> "sequencer2", + ) + override protected lazy val newOldMediators: Map[String, String] = Map( + "mediator3" -> "mediator1", + "mediator4" -> "mediator2", ) - override protected lazy val newOldMediators: Map[String, String] = Map("mediator2" -> "mediator1") override protected lazy val upgradeTime: CantonTimestamp = CantonTimestamp.Epoch.plusSeconds(30) override lazy val environmentDefinition: EnvironmentDefinition = - EnvironmentDefinition.P2S2M2_Config + EnvironmentDefinition.P2S4M4_Config .withNetworkBootstrap { implicit env => - new NetworkBootstrapper(S1M1) + new NetworkBootstrapper(S2M2) } .addConfigTransforms(configTransforms*) .addConfigTransforms( @@ -66,8 +75,11 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { private var fixture: Fixture = _ + private lazy val handshakeFailureWarn = + s"Unable to perform handshake with ${fixture.newPsid}" + "Sequencers" should { - "contact their successor and update metrics to reflect status" in { implicit env => + "have contact metrics set to 0 initially" in { implicit env => import env.* fixture = fixtureWithDefaults() @@ -84,11 +96,108 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { eventually() { getLsuSuccessorContactStatusMetricValues(sequencer1) shouldBe Map(fixture.newPsid -> 0) } + } + + "not update the metric if psid is incorrect" in { implicit env => + import env.* + + // Incorrect announcement of the successor: wrong psid + loggerFactory.assertEventuallyLogsSeq(SuppressionRule.Level(Level.WARN))( + sequencer1.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer1.id, + // announced successor is itself -> wrong psid + endpoints = sequencer1.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ), + LogEntry.assertLogSeq( + Seq( + ( + _.warningMessage should include( + s"Error when contacting successor: expecting psid to be ${fixture.newPsid} but found ${fixture.currentPsid}" + ), + "warning on sequencer", + ), + ( + _.warningMessage should include( + "Connection internal-sequencer-connection-sequencer1-0: Invalid synchronizer" + ), + "connection pool warn on p1", + ), + ( + _.warningMessage should include( + "Connection internal-sequencer-connection-sequencer1-0: Invalid synchronizer" + ), + "connection pool warn on p2", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p1", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p2", + ), + ) + ), + ) + + // metric is not updated + getLsuSuccessorContactStatusMetricValues(sequencer1) shouldBe Map(fixture.newPsid -> 0) + } + + "not update the metric if sequencer id is incorrect" in { implicit env => + import env.* + + // Incorrect announcement of the successor: wrong sequencer id + loggerFactory.assertEventuallyLogsSeq(SuppressionRule.Level(Level.WARN))( + sequencer1.topology.lsu.sequencer_successors.propose_successor( + sequencerId = sequencer1.id, + // announced successor is another sequencer -> wrong sequencer id + endpoints = sequencer4.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + successorSynchronizerId = fixture.newPsid, + ), + LogEntry.assertLogSeq( + Seq( + ( + _.warningMessage should include( + s"Error when contacting successor: expecting sequencer id to be ${sequencer1.id} but found ${sequencer2.id}" + ), + "warning on sequencer", + ), + ( + _.warningMessage should include( + "Validation failure: Connection is not on expected sequencer" + ), + "connection pool warn on p1", + ), + ( + _.warningMessage should include( + "Validation failure: Connection is not on expected sequencer" + ), + "connection pool warn on p2", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p1", + ), + ( + _.warningMessage should include(handshakeFailureWarn), + "handshake failure on p2", + ), + ) + ), + ) + + // metric is not updated + getLsuSuccessorContactStatusMetricValues(sequencer1) shouldBe Map(fixture.newPsid -> 0) + } + + "update the metric when the successor is correct" in { implicit env => + import env.* - // Announcement of the successor sequencer1.topology.lsu.sequencer_successors.propose_successor( sequencerId = sequencer1.id, - endpoints = sequencer2.sequencerConnection.endpoints.map(_.toURI(useTls = false)), + endpoints = sequencer3.sequencerConnection.endpoints.map(_.toURI(useTls = false)), successorSynchronizerId = fixture.newPsid, ) @@ -103,7 +212,7 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { participants.all.forall(_.synchronizers.is_connected(fixture.newPsid)) shouldBe true } - waitForTargetTimeOnSequencer(sequencer2, environment.clock.now, logger) + waitForTargetTimeOnSequencer(sequencer3, environment.clock.now, logger) oldSynchronizerNodes.all.stop() participant1.health.ping(participant2) @@ -116,22 +225,22 @@ final class LsuSequencerContactSuccessorIntegrationTest extends LsuBase { val upgradeTime2 = fixture.upgradeTime.plusSeconds(30) val upgradeTime3 = upgradeTime2.plusSeconds(30) - sequencer2.topology.lsu.announcement.propose(psid2, upgradeTime2) + sequencer3.topology.lsu.announcement.propose(psid2, upgradeTime2) eventually() { - getLsuSuccessorContactStatusMetricValues(sequencer2) shouldBe Map(psid2 -> 0) + getLsuSuccessorContactStatusMetricValues(sequencer3) shouldBe Map(psid2 -> 0) } - sequencer2.topology.lsu.announcement.propose(psid3, upgradeTime3) + sequencer3.topology.lsu.announcement.propose(psid3, upgradeTime3) eventually() { - getLsuSuccessorContactStatusMetricValues(sequencer2) shouldBe Map(psid2 -> 0, psid3 -> 0) + getLsuSuccessorContactStatusMetricValues(sequencer3) shouldBe Map(psid2 -> 0, psid3 -> 0) } - sequencer2.topology.lsu.announcement.revoke(psid3, upgradeTime3) + sequencer3.topology.lsu.announcement.revoke(psid3, upgradeTime3) eventually() { - getLsuSuccessorContactStatusMetricValues(sequencer2) shouldBe Map(psid2 -> 0, psid3 -> -1) + getLsuSuccessorContactStatusMetricValues(sequencer3) shouldBe Map(psid2 -> 0, psid3 -> -1) } } } diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala index e1f4cb1bf5..f2aa2ef39c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuSessionSigningKeysIntegrationTest.scala @@ -53,7 +53,7 @@ final class LsuSessionSigningKeysIntegrationTest override protected def configTransforms: Seq[ConfigTransform] = super.configTransforms :+ ConfigTransforms.setSigningKeysIfPV35OrHigher( - SessionSigningKeysConfig.default + SessionSigningKeysConfig.enabled ) override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P1S2M2_Config diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala index 023278f24f..bc76f6a23d 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/upgrade/lsu/LsuTrafficTransferRestartIntegrationTest.scala @@ -25,7 +25,8 @@ import java.time.Duration * * Topology: * - P1 connected to S1 - * - P2 connected to S2 + * - P2 connected to S1 and S2 with threshold=2 + * - P3 connected to S2, offboarded before the LSU * * This test: * - Generates purchased/consumed traffic by performing some activity on the predecessor @@ -74,7 +75,9 @@ final class LsuTrafficTransferRestartIntegrationTest extends LsuBase with Traffi changeDynamicSynchronizerParameters = false, ) participant1.synchronizers.connect_by_config(synchronizerConnectionConfig(sequencer1)) - participant2.synchronizers.connect_by_config(synchronizerConnectionConfig(sequencer2)) + participant2.synchronizers.connect_by_config( + synchronizerConnectionConfig(Seq(sequencer1, sequencer2), 2) + ) participant3.synchronizers.connect_by_config(synchronizerConnectionConfig(sequencer2)) participants.all.dars.upload(CantonExamplesPath) @@ -143,6 +146,11 @@ final class LsuTrafficTransferRestartIntegrationTest extends LsuBase with Traffi } } + /* + Regression test. + This ensures that checks about the number of traffic entries are consistent + between export and import. + */ "offboard participant3" in { implicit env => import env.* diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala index ac03f18445..d3c27a9299 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/tests/version/MultipleProtocolVersionReassignmentIntegrationTest.scala @@ -51,7 +51,7 @@ sealed trait MultipleProtocolVersionReassignmentIntegrationTest _.focus(_.parameters.minimumProtocolVersion) .replace(Some(ParticipantProtocolVersion(beforeLastStable))) }, - ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag, + ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag, ) .addConfigTransforms(ConfigTransforms.dontWarnOnDeprecatedPV*) .withSetup { implicit env => diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala index 575a195c15..a54dd65974 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/PartyToParticipantDeclarative.scala @@ -16,7 +16,11 @@ import com.digitalasset.canton.integration.util.PartyToParticipantDeclarativeCom PartyHostingState, Serial, } -import com.digitalasset.canton.integration.{PartyTopologyUtils, TestEnvironment} +import com.digitalasset.canton.integration.{ + CantonTestEnvironment, + PartyTopologyUtils, + TestEnvironment, +} import com.digitalasset.canton.topology.* import com.digitalasset.canton.topology.transaction.{ HostingParticipant, @@ -167,7 +171,7 @@ class PartyToParticipantDeclarative( Map[PhysicalSynchronizerId, PartyHostingState], ], onboarding: Boolean, -)(implicit executionContext: ExecutionContext, env: TestEnvironment) +)(implicit executionContext: ExecutionContext, env: TestEnvironment[?]) extends PartyToParticipantDeclarativeCommon[Party] { override protected def partyReference: (PartyToParticipant, HashingSchemeVersion) => Party = @@ -413,7 +417,7 @@ object PartyToParticipantDeclarative { ], forceFlags: ForceFlags = ForceFlags.none, onboarding: Boolean = false, // participants added in target topology are marked as onboarding - )(implicit executionContext: ExecutionContext, env: TestEnvironment): Unit = { + )(implicit executionContext: ExecutionContext, env: CantonTestEnvironment): Unit = { val participantReference = participants.headOption.getOrElse( fail("No participant set in PartyToParticipantDeclarative") ) @@ -462,7 +466,7 @@ object PartyToParticipantDeclarative { threshold: PositiveInt, hosting: Set[(ParticipantId, ParticipantPermission)], forceFlags: ForceFlags = ForceFlags.none, - )(implicit executionContext: ExecutionContext, env: TestEnvironment): Unit = + )(implicit executionContext: ExecutionContext, env: CantonTestEnvironment): Unit = apply(participants, Set(synchronizerId))( Map(party.partyId -> owningParticipant), Map(party -> Map(synchronizerId -> (threshold, hosting))), @@ -492,7 +496,7 @@ class PartiesAllocator( )( newParties: Seq[(String, ParticipantId)], val targetTopology: Map[String, Map[PhysicalSynchronizerId, PartyHostingState]], -)(implicit executionContext: ExecutionContext, env: TestEnvironment) +)(implicit executionContext: ExecutionContext, env: TestEnvironment[?]) extends PartyToParticipantDeclarativeCommon[String] { override def externalParties: Set[ExternalParty] = Set.empty @@ -627,7 +631,7 @@ object PartiesAllocator { ], )(implicit executionContext: ExecutionContext, - env: TestEnvironment, + env: TestEnvironment[?], partyKind: PartyKind, ): Seq[Party] = new PartiesAllocator(participants)( diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala index 9bed31e4e3..015dd54c3c 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/integration/util/TestSubmissionService.scala @@ -355,7 +355,7 @@ class TestSubmissionService( response: Response = contractOpt match { case Some(contract) => - Response.ContractFound(contract, Hash.HashingMethod.UpgradeFriendly, _ => true) + Response.ContractFound(contract, Hash.HashingMethod.TypedNormalForm, _ => true) case None => Response.ContractNotFound } @@ -378,7 +378,22 @@ class TestSubmissionService( for { cidO <- keyResolver.resolveKey(key)(traceContext) contracts <- cidO.toList.parTraverse(contractResolver(_)(traceContext)) - r <- resolve(resume(contracts.flatten.toVector, NeedKeyProgression.Finished)) + r <- resolve( + resume( + ResultNeedKey.Response( + contracts.flatten + .map(fci => + ResultNeedKey.Response.AuthenticableFatContractInstance( + fci, + Hash.HashingMethod.TypedNormalForm, + _ => true, + ) + ) + .toVector, + NeedKeyProgression.Finished, + ) + ) + ) } yield r case ResultInterruption(continue, _) => diff --git a/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala b/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala index 2eda2caf52..3bfce40700 100644 --- a/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala +++ b/canton/community/app/src/test/scala/com/digitalasset/canton/util/ReleaseUtils.scala @@ -15,9 +15,11 @@ import com.digitalasset.canton.version.{ ProtocolVersionCompatibility, ReleaseVersion, } +import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import java.nio.file.{Files, Paths} import scala.collection.concurrent.TrieMap +import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ExecutionContext, Future} /** A collection of small utilities for tests that have no obvious home */ @@ -119,6 +121,8 @@ object ReleaseUtils { private val releasesRetrieval: TrieMap[ReleaseVersion, Future[String]] = TrieMap.empty private val lock = new Mutex() + val DefaultReleaseDownloadTimeout: FiniteDuration = 5.minutes + /** If the .tar.gz corresponding to release is not found locally, attempts to download it from * artifactory. Then, extract the .tar.gz file. * @param release diff --git a/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto b/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto index ffe2febce4..3a9cfeda96 100644 --- a/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto +++ b/canton/community/base/src/main/protobuf/com/digitalasset/canton/protocol/v30/topology.proto @@ -67,7 +67,7 @@ message Enums { // in model conformance in PV 33 PARTICIPANT_FEATURE_FLAG_PV33_EXTERNAL_SIGNING_LOCAL_CONTRACT_IN_SUBVIEW = 1; // This flag indicates that the participant supports reassignments between synchronizers. - PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER = 2; + PARTICIPANT_FEATURE_FLAG_ENABLE_MULTI_SYNCHRONIZER = 2; } } diff --git a/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto b/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto index dba27f4fb0..946b23f077 100644 --- a/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto +++ b/canton/community/base/src/main/protobuf/com/digitalasset/canton/topology/admin/v30/common.proto @@ -51,3 +51,15 @@ message StoreId { Temporary temporary = 3; } } + +message SynchronizerPredecessor { + // Id of the predecessor. + string predecessor_physical_id = 1; + + // When the upgrade happened/is supposed to happen. + google.protobuf.Timestamp upgrade_time = 2; + + // Whether the participant is performing a late synchronizer upgrade. + // default: false + bool is_late_upgrade = 3; +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala index eeaedaf311..eeed16b3af 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/AuthServiceConfig.scala @@ -153,6 +153,7 @@ object AuthServiceConfig { override val privileged: Boolean = false, accessLevel: AccessLevel = AccessLevel.Wildcard, override val users: Seq[AuthorizedUser] = Seq.empty, + override val maxTokenLife: config.NonNegativeDuration = NonNegativeDuration(Duration.Inf), ) extends AuthServiceConfig { @SuppressWarnings(Array("org.wartremover.warts.Null")) private def verifier( @@ -198,6 +199,7 @@ object AuthServiceConfig { override val privileged: Boolean = false, accessLevel: AccessLevel = AccessLevel.Wildcard, override val users: Seq[AuthorizedUser] = Seq.empty, + override val maxTokenLife: config.NonNegativeDuration = NonNegativeDuration(Duration.Inf), ) extends AuthServiceConfig { @SuppressWarnings(Array("org.wartremover.warts.Null")) private def verifier( @@ -243,6 +245,7 @@ object AuthServiceConfig { override val privileged: Boolean = false, accessLevel: AccessLevel = AccessLevel.Wildcard, override val users: Seq[AuthorizedUser] = Seq.empty, + override val maxTokenLife: config.NonNegativeDuration = NonNegativeDuration(Duration.Inf), ) extends AuthServiceConfig { private def verifier( jwksCacheConfig: JwksCacheConfig, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala index 1f9cfbfb0d..f57273748a 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/BaseCantonConfig.scala @@ -31,6 +31,9 @@ object BaseCantonConfig { lazy implicit final val connectionAllocationReader: ConfigReader[ConnectionAllocation] = deriveReader[ConnectionAllocation] + lazy implicit final val partitionConfigReader: ConfigReader[PartitionConfig] = + deriveReader[PartitionConfig] + lazy implicit final val dbParamsReader: ConfigReader[DbParametersConfig] = deriveReader[DbParametersConfig] @@ -57,6 +60,9 @@ object BaseCantonConfig { lazy implicit final val connectionAllocationWriter: ConfigWriter[ConnectionAllocation] = deriveWriter[ConnectionAllocation] + lazy implicit final val partitionConfigWriter: ConfigWriter[PartitionConfig] = + deriveWriter[PartitionConfig] + lazy implicit final val dbParamsWriter: ConfigWriter[DbParametersConfig] = deriveWriter[DbParametersConfig] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala index db71c66ba7..68fd51ac1f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/SessionSigningKeysConfig.scala @@ -97,7 +97,7 @@ final case class SessionSigningKeysConfig( object SessionSigningKeysConfig { val disabled: SessionSigningKeysConfig = SessionSigningKeysConfig(enabled = false) - val default: SessionSigningKeysConfig = SessionSigningKeysConfig(enabled = true) + val enabled: SessionSigningKeysConfig = SessionSigningKeysConfig(enabled = true) /** Short test-only configuration: durations are small enough to trigger key rotation and validity * edge cases within a test. diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala index 54e200bd1d..6d1323f657 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/StorageConfig.scala @@ -73,6 +73,7 @@ final case class DbParametersConfig( // Make the default settings a part of repeatable migrations repeatableMigrationsPaths: Seq[String] = Seq.empty, + partitions: PartitionConfig = PartitionConfig(), ) extends PrettyPrinting { override protected def pretty: Pretty[DbParametersConfig] = prettyOfClass( @@ -203,6 +204,13 @@ final case class ConnectionAllocation( ) } +/** @param initialBftOrdererTablesPartitionSize + * Initial partition size for bft-orderer tables. Note that this config is only read once, during + * the initial database setup and later changes to this value won't have any effect. This is also + * only used in Postgres setups. + */ +final case class PartitionConfig(initialBftOrdererTablesPartitionSize: Int = 1500) + object DbParametersConfig { private val defaultWarnOnSlowQueryInterval: PositiveFiniteDuration = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala index 84a27d9162..f7df89d3c9 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/config/TestingConfigInternal.scala @@ -58,9 +58,10 @@ import com.digitalasset.canton.metrics.MetricsFactoryType.External * @param useLegacyContractIdVersionV11 * Uses contract id version V11 for testing purposes. * @param warnOnJwtScopeUsage - * When true, we log a warning on the first time a JWT token with scope is sent to the Ledger API - * This pattern is currently discouraged for security reasons but will be re-enabled in future - * versions. + * When true, we log a warning on the first time a JWT with a scope but no audience is sent to + * the Ledger API, provided that no explicit targetAudience or targetScope is configured. (In + * other cases, other appropriate warnings are raised on startup if needed). Such tokens are + * currently discouraged for security reasons and will be removed from use in future versions. */ final case class TestingConfigInternal( testSequencerClientFor: Set[TestSequencerClientFor] = Set.empty, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala index 5cce8ef7ac..3b1d6377eb 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/CryptoApi.scala @@ -44,6 +44,7 @@ import com.digitalasset.canton.health.{ import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, LifeCycle} import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.Storage @@ -76,6 +77,7 @@ sealed trait BaseCrypto extends NamedLogging { def privateCrypto: CryptoPrivateApi def cryptoPrivateStore: CryptoPrivateStore def cryptoPublicStore: CryptoPublicStore + def cryptoMetrics: CryptoMetrics /** Helper method to generate a new signing key pair and store the public key in the public store * as well. @@ -116,6 +118,7 @@ class Crypto private[crypto] ( override val privateCrypto: CryptoPrivateApi, override val cryptoPrivateStore: CryptoPrivateStore, override val cryptoPublicStore: CryptoPublicStore, + override val cryptoMetrics: CryptoMetrics, override val timeouts: ProcessingTimeout, override val loggerFactory: NamedLoggerFactory, )(override implicit val ec: ExecutionContext) @@ -155,12 +158,15 @@ final case class SynchronizerCrypto( new SynchronizerCryptoPrivateApi( staticSynchronizerParameters, crypto.privateCrypto, + crypto.cryptoMetrics.signingMetrics, + crypto.cryptoMetrics.decryptionMetrics, crypto.timeouts, crypto.loggerFactory, ) override val cryptoPrivateStore: CryptoPrivateStore = crypto.cryptoPrivateStore override val cryptoPublicStore: CryptoPublicStore = crypto.cryptoPublicStore + override val cryptoMetrics: CryptoMetrics = crypto.cryptoMetrics override protected val loggerFactory: NamedLoggerFactory = crypto.loggerFactory } @@ -385,6 +391,7 @@ object Crypto { releaseProtocolVersion: ReleaseProtocolVersion, futureSupervisor: FutureSupervisor, clock: Clock, + cryptoMetrics: CryptoMetrics, executionContext: ExecutionContext, timeouts: ProcessingTimeout, batchingConfig: BatchingConfig, @@ -431,6 +438,7 @@ object Crypto { publicKeyConversionCacheConfig, cryptoPrivateStore, cryptoPublicStore, + cryptoMetrics, timeouts, loggerFactory, ) @@ -478,6 +486,7 @@ object Crypto { kmsSchemes.encryptionSchemes, cryptoPublicStore, kmsCryptoPrivateStore, + cryptoMetrics, timeouts, loggerFactory, ) @@ -488,6 +497,7 @@ object Crypto { sessionEncryptionKeyCacheConfig, publicKeyConversionCacheConfig, cryptoSchemes, + cryptoMetrics, loggerFactory, ) .toEitherT[FutureUnlessShutdown] @@ -496,6 +506,7 @@ object Crypto { kmsPrivateCrypto, kmsCryptoPrivateStore, cryptoPublicStore, + cryptoMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala index c496c6e69e..b0ffe0ec01 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Encryption.scala @@ -16,6 +16,7 @@ import com.digitalasset.canton.crypto.store.{CryptoPrivateStoreError, CryptoPriv import com.digitalasset.canton.error.{CantonBaseError, CantonErrorGroups} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.metrics.DecryptionMetrics import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.{ CryptoParseAndValidationError, @@ -36,14 +37,7 @@ import scala.concurrent.ExecutionContext /** Encryption operations that do not require access to a private key store but operates with * provided keys. */ -trait EncryptionOps { - - private[crypto] def decryptWithInternal[M]( - encrypted: AsymmetricEncrypted[M], - privateKey: EncryptionPrivateKey, - )( - deserialize: ByteString => Either[DeserializationError, M] - ): Either[DecryptionError, M] +trait EncryptionOps extends DecryptionMetricsSupport { def defaultSymmetricKeyScheme: SymmetricKeyScheme @@ -76,11 +70,6 @@ trait EncryptionOps { encryptionAlgorithmSpec: EncryptionAlgorithmSpec = encryptionAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[EncryptionError, AsymmetricEncrypted[M]] - /** Decrypts a message encrypted using `encryptWith` */ - def decryptWith[M](encrypted: AsymmetricEncrypted[M], privateKey: EncryptionPrivateKey)( - deserialize: ByteString => Either[DeserializationError, M] - ): Either[DecryptionError, M] = decryptWithInternal(encrypted, privateKey)(deserialize) - /** Encrypts the bytes of the serialized message using the given symmetric key. Where the message * embedded protocol version determines the message serialization. */ @@ -97,6 +86,24 @@ trait EncryptionOps { symmetricKey: SymmetricKey, ): Either[EncryptionError, ByteString] + /** Decrypts a message encrypted using `encryptWith`. Records latency for the decryption + * operation. + */ + def decryptWith[M](encrypted: AsymmetricEncrypted[M], privateKey: EncryptionPrivateKey)( + deserialize: ByteString => Either[DeserializationError, M] + ): Either[DecryptionError, M] = + decryptionMetrics.decryptLatency.time(decryptWithInternal(encrypted, privateKey)(deserialize)) + + /** Internal decryption primitive implemented by concrete backends. This bypasses higher-level + * wrappers (e.g. metrics and validation) and should only be used by internal decryption logic. + */ + private[crypto] def decryptWithInternal[M]( + encrypted: AsymmetricEncrypted[M], + privateKey: EncryptionPrivateKey, + )( + deserialize: ByteString => Either[DeserializationError, M] + ): Either[DecryptionError, M] + /** Decrypts a message encrypted using `encryptWith` */ def decryptWith[M](encrypted: Encrypted[M], symmetricKey: SymmetricKey)( deserialize: ByteString => Either[DeserializationError, M] @@ -105,17 +112,10 @@ trait EncryptionOps { } /** Encryption operations that require access to stored private keys. */ -trait EncryptionPrivateOps { +trait EncryptionPrivateOps extends DecryptionMetricsSupport { def encryptionSchemes: EncryptionCryptoSchemes - /** Decrypts an encrypted message using the referenced private encryption key */ - def decrypt[M](encrypted: AsymmetricEncrypted[M])( - deserialize: ByteString => Either[DeserializationError, M] - )(implicit - traceContext: TraceContext - ): EitherT[FutureUnlessShutdown, DecryptionError, M] - /** Generates a new encryption key pair with the given scheme and optional name, stores the * private key and returns the public key. */ @@ -125,6 +125,29 @@ trait EncryptionPrivateOps { )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, EncryptionKeyGenerationError, EncryptionPublicKey] + + /** Decrypts an encrypted message using the referenced private encryption key. Records latency for + * the decryption operation. + */ + def decrypt[M](encrypted: AsymmetricEncrypted[M])( + deserialize: ByteString => Either[DeserializationError, M] + )(implicit + executionContext: ExecutionContext, + traceContext: TraceContext, + ): EitherT[FutureUnlessShutdown, DecryptionError, M] = + EitherTUtil.timed(decryptionMetrics.decryptLatency)( + decryptInternal(encrypted)(deserialize) + ) + + /** Internal decryption primitive implemented by concrete backends. This bypasses higher-level + * wrappers (e.g. metrics and validation) and should only be used by internal decryption logic. + */ + private[crypto] def decryptInternal[M](encrypted: AsymmetricEncrypted[M])( + deserialize: ByteString => Either[DeserializationError, M] + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, DecryptionError, M] + } /** A default implementation with a private key store */ @@ -137,7 +160,7 @@ trait EncryptionPrivateStoreOps extends EncryptionPrivateOps { protected val encryptionOps: EncryptionOps /** Decrypts an encrypted message using the referenced private encryption key */ - override def decrypt[M](encryptedMessage: AsymmetricEncrypted[M])( + override private[crypto] def decryptInternal[M](encryptedMessage: AsymmetricEncrypted[M])( deserialize: ByteString => Either[DeserializationError, M] )(implicit tc: TraceContext): EitherT[FutureUnlessShutdown, DecryptionError, M] = store @@ -170,6 +193,11 @@ trait EncryptionPrivateStoreOps extends EncryptionPrivateOps { } +/** Provides decryption-related metrics. */ +trait DecryptionMetricsSupport { + def decryptionMetrics: DecryptionMetrics +} + /** A tag to denote encrypted data. */ final case class Encrypted[+M] private[crypto] (ciphertext: ByteString) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala index 49524eee86..7fdcbdcc1c 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/Signing.scala @@ -23,6 +23,7 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.error.{CantonBaseError, CantonErrorGroups} import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.metrics.SigningMetrics import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.serialization.{ CryptoParseAndValidationError, @@ -33,7 +34,7 @@ import com.digitalasset.canton.serialization.{ import com.digitalasset.canton.store.db.DbDeserializationException import com.digitalasset.canton.topology.{Member, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.EitherUtil +import com.digitalasset.canton.util.{EitherTUtil, EitherUtil} import com.digitalasset.canton.version.* import com.google.common.annotations.VisibleForTesting import com.google.protobuf.ByteString @@ -50,7 +51,7 @@ import scala.concurrent.ExecutionContext /** Signing operations that do not require access to a private key store but operates with provided * keys. */ -trait SigningOps { +trait SigningOps extends SigningMetricsSupport { def signatureVerificationParallelism: PositiveInt @@ -69,14 +70,32 @@ trait SigningOps { usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[SigningError, Signature] = - signBytes(hash.getCryptographicEvidence, signingKey, usage, signingAlgorithmSpec) + signingMetrics.signingLatency.time( + signBytesInternal(hash.getCryptographicEvidence, signingKey, usage, signingAlgorithmSpec) + ) - /** Preferably, we sign a hash; however, we also allow signing arbitrary bytes when necessary. */ + /** Signs raw bytes using the private signing key. Convenience wrapper used when signing + * non-hashed data. + */ protected[crypto] def signBytes( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, + )(implicit traceContext: TraceContext): Either[SigningError, Signature] = + signingMetrics.signingLatency.time( + signBytesInternal(bytes, signingKey, usage, signingAlgorithmSpec) + ) + + /** Internal signing primitive implemented by concrete backends. Performs the actual cryptographic + * signing of raw bytes. This bypasses higher-level wrappers (e.g. metrics and validation) and + * should only be used by internal signing logic. + */ + private[crypto] def signBytesInternal( + bytes: ByteString, + signingKey: SigningPrivateKey, + usage: NonEmpty[Set[SigningKeyUsage]], + signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[SigningError, Signature] /** Confirms if the provided signature is a valid signature of the payload using the public key */ @@ -97,27 +116,51 @@ trait SigningOps { } /** Signing operations that require access to stored private keys. */ -trait SigningPrivateOps { +trait SigningPrivateOps extends SigningMetricsSupport { def signingSchemes: SigningCryptoSchemes - /** Signs the given hash using the referenced private signing key. */ + /** Signs the given hash using the referenced private signing key. Latency of the signing + * operation is recorded for all outcomes (successful signatures and signing failures). + */ def sign( hash: Hash, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingSchemes.algorithmSpecs.default, )(implicit - tc: TraceContext + ec: ExecutionContext, + tc: TraceContext, ): EitherT[FutureUnlessShutdown, SigningError, Signature] = - signBytes(hash.getCryptographicEvidence, signingKeyId, usage, signingAlgorithmSpec) + EitherTUtil.timed(signingMetrics.signingLatency)( + signBytesInternal(hash.getCryptographicEvidence, signingKeyId, usage, signingAlgorithmSpec) + ) - /** Signs the byte string directly, however it is encouraged to sign a hash. */ + /** Signs the byte string directly, however it is encouraged to sign a hash. Latency of the + * signing operation is recorded for all outcomes (successful signatures and signing failures). + */ def signBytes( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingSchemes.algorithmSpecs.default, + )(implicit + ec: ExecutionContext, + tc: TraceContext, + ): EitherT[FutureUnlessShutdown, SigningError, Signature] = + EitherTUtil.timed(signingMetrics.signingLatency)( + signBytesInternal(bytes, signingKeyId, usage, signingAlgorithmSpec) + ) + + /** Internal signing primitive that produces a signature for the given bytes. This bypasses + * higher-level wrappers (e.g. metrics and validation) and should only be used by internal + * signing logic. + */ + private[crypto] def signBytesInternal( + bytes: ByteString, + signingKeyId: Fingerprint, + usage: NonEmpty[Set[SigningKeyUsage]], + signingAlgorithmSpec: SigningAlgorithmSpec = signingSchemes.algorithmSpecs.default, )(implicit tc: TraceContext): EitherT[FutureUnlessShutdown, SigningError, Signature] /** Generates a new signing key pair with the given scheme and optional name, stores the private @@ -133,6 +176,11 @@ trait SigningPrivateOps { } +/** Provides signing-related metrics. */ +trait SigningMetricsSupport { + def signingMetrics: SigningMetrics +} + /** A default implementation with a private key store */ trait SigningPrivateStoreOps extends SigningPrivateOps { @@ -142,7 +190,7 @@ trait SigningPrivateStoreOps extends SigningPrivateOps { protected val signingOps: SigningOps - override def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala index 73dc377cf8..a661111b2e 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SyncCryptoApiParticipantProvider.scala @@ -19,7 +19,7 @@ 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.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.KmsMetrics +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.topology.* @@ -54,7 +54,7 @@ class SyncCryptoApiParticipantProvider( val ips: IdentityProvidingServiceClient, val crypto: Crypto, cryptoConfig: CryptoConfig, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, publicKeyConversionCacheConfig: CacheConfig, timeouts: ProcessingTimeout, futureSupervisor: FutureSupervisor, @@ -93,7 +93,7 @@ class SyncCryptoApiParticipantProvider( staticSynchronizerParameters, SynchronizerCrypto(crypto, staticSynchronizerParameters), cryptoConfig, - kmsMetrics, + cryptoMetrics, publicKeyConversionCacheConfig, timeouts, futureSupervisor, @@ -426,7 +426,7 @@ object SynchronizerCryptoClient { staticSynchronizerParameters: StaticSynchronizerParameters, synchronizerCrypto: SynchronizerCrypto, cryptoConfig: CryptoConfig, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, publicKeyConversionCacheConfig: CacheConfig, timeouts: ProcessingTimeout, futureSupervisor: FutureSupervisor, @@ -440,7 +440,7 @@ object SynchronizerCryptoClient { member, synchronizerCrypto, cryptoConfig, - kmsMetrics, + cryptoMetrics, publicKeyConversionCacheConfig, futureSupervisor, timeouts, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala index 005b8873e6..71abde8dd8 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPrivateApi.scala @@ -10,6 +10,7 @@ import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.health.ComponentHealthState import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.tracing.TraceContext @@ -26,6 +27,8 @@ import scala.concurrent.ExecutionContext final class SynchronizerCryptoPrivateApi( override val staticSynchronizerParameters: StaticSynchronizerParameters, privateCrypto: CryptoPrivateApi, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContext) @@ -36,7 +39,7 @@ final class SynchronizerCryptoPrivateApi( override private[crypto] def getInitialHealthState: ComponentHealthState = privateCrypto.getInitialHealthState - override def decrypt[M]( + override private[crypto] def decryptInternal[M]( encrypted: AsymmetricEncrypted[M] )( deserialize: ByteString => Either[DeserializationError, M] @@ -67,7 +70,7 @@ final class SynchronizerCryptoPrivateApi( override def signingSchemes: SigningCryptoSchemes = privateCrypto.signingSchemes - override def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala index ad04c3ac76..fbf8d6ba37 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/SynchronizerCryptoPureApi.scala @@ -5,6 +5,7 @@ package com.digitalasset.canton.crypto import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.metrics.{DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.tracing.TraceContext @@ -111,6 +112,8 @@ final class SynchronizerCryptoPureApi( symmetricKey: SymmetricKey, ): Either[EncryptionError, ByteString] = pureCrypto.encryptSymmetricWith(data, symmetricKey) + override def decryptionMetrics: DecryptionMetrics = pureCrypto.decryptionMetrics + override def decryptWith[M]( encrypted: Encrypted[M], symmetricKey: SymmetricKey, @@ -145,11 +148,14 @@ final class SynchronizerCryptoPureApi( override def signingAlgorithmSpecs: CryptoScheme[SigningAlgorithmSpec] = pureCrypto.signingAlgorithmSpecs - override protected[crypto] def signBytes( + override def signingMetrics: SigningMetrics = pureCrypto.signingMetrics + + override private[crypto] def signBytesInternal( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], signingAlgorithmSpec: SigningAlgorithmSpec = signingAlgorithmSpecs.default, )(implicit traceContext: TraceContext): Either[SigningError, Signature] = pureCrypto.signBytes(bytes, signingKey, usage, signingAlgorithmSpec) + } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala index c71fd873d6..979ccdb7f6 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JceCrypto.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.config.{ import com.digitalasset.canton.crypto.store.{CryptoPrivateStore, CryptoPublicStore} import com.digitalasset.canton.crypto.{Crypto, CryptoSchemes} import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.util.EitherUtil import scala.concurrent.ExecutionContext @@ -26,6 +27,7 @@ object JceCrypto { publicKeyConversionCacheConfig: CacheConfig, cryptoPrivateStore: CryptoPrivateStore, cryptoPublicStore: CryptoPublicStore, + cryptoMetrics: CryptoMetrics, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, )(implicit @@ -46,6 +48,7 @@ object JceCrypto { sessionEncryptionKeyCacheConfig, publicKeyConversionCacheConfig, cryptoSchemes, + cryptoMetrics, loggerFactory, ) privateCrypto = @@ -54,6 +57,8 @@ object JceCrypto { signingSchemes = cryptoSchemes.signingSchemes, encryptionSchemes = cryptoSchemes.encryptionSchemes, store = cryptoPrivateStoreExtended, + signingMetrics = cryptoMetrics.signingMetrics, + decryptionMetrics = cryptoMetrics.decryptionMetrics, timeouts = timeouts, loggerFactory = loggerFactory, ) @@ -62,6 +67,7 @@ object JceCrypto { privateCrypto, cryptoPrivateStore, cryptoPublicStore, + cryptoMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala index 16c16581f3..3492ed9b0d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePrivateCrypto.scala @@ -13,6 +13,7 @@ import com.digitalasset.canton.crypto.store.CryptoPrivateStoreExtended import com.digitalasset.canton.health.ComponentHealthState import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.tracing.TraceContext import com.google.protobuf.ByteString import org.bouncycastle.asn1.DEROctetString @@ -45,6 +46,8 @@ class JcePrivateCrypto( override val signingSchemes: SigningCryptoSchemes, override val encryptionSchemes: EncryptionCryptoSchemes, override protected val store: CryptoPrivateStoreExtended, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, )(override implicit val ec: ExecutionContext) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala index b704a21306..46d0ed8c11 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/jce/JcePureCrypto.scala @@ -22,6 +22,7 @@ import com.digitalasset.canton.crypto.HmacError.{ import com.digitalasset.canton.crypto.deterministic.encryption.DeterministicRandom import com.digitalasset.canton.crypto.{SignatureCheckError, *} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{CryptoMetrics, DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.serialization.{ DefaultDeserializationError, DeserializationError, @@ -90,6 +91,8 @@ class JcePureCrypto( publicKeyConversionCacheConfig: CacheConfig, privateKeyConversionCacheTtl: Option[FiniteDuration], override val signatureVerificationParallelism: PositiveInt, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext) extends CryptoPureApi @@ -318,7 +321,7 @@ class JcePureCrypto( SymmetricKey.create(CryptoKeyFormat.Raw, bytes.unwrap, scheme) } - override protected[crypto] def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKey: SigningPrivateKey, usage: NonEmpty[Set[SigningKeyUsage]], @@ -850,6 +853,7 @@ object JcePureCrypto { sessionEncryptionKeyCacheConfig: SessionEncryptionKeyCacheConfig, publicKeyConversionCacheConfig: CacheConfig, cryptoSchemes: CryptoSchemes, + cryptoMetrics: CryptoMetrics, loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext): Either[String, JcePureCrypto] = { @@ -889,6 +893,8 @@ object JcePureCrypto { publicKeyConversionCacheConfig = publicKeyConversionCacheConfig, privateKeyConversionCacheTtl = minimumPrivateKeyCacheDuration, signatureVerificationParallelism = config.parallelism.signatureVerificationParallelism, + signingMetrics = cryptoMetrics.signingMetrics, + decryptionMetrics = cryptoMetrics.decryptionMetrics, loggerFactory = loggerFactory, ) } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala index cfc2619b73..6675366855 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/provider/kms/KmsPrivateCrypto.scala @@ -24,6 +24,7 @@ import com.digitalasset.canton.health.{ } import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{CryptoMetrics, DecryptionMetrics, SigningMetrics} import com.digitalasset.canton.serialization.DeserializationError import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.{ByteString256, ByteString4096} @@ -37,6 +38,8 @@ class KmsPrivateCrypto( private[kms] val publicStore: CryptoPublicStore, override val signingSchemes: SigningCryptoSchemes, override val encryptionSchemes: EncryptionCryptoSchemes, + override val signingMetrics: SigningMetrics, + override val decryptionMetrics: DecryptionMetrics, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext) @@ -137,7 +140,7 @@ class KmsPrivateCrypto( ) } yield publicKey - def signBytes( + override private[crypto] def signBytesInternal( bytes: ByteString, signingKeyId: Fingerprint, usage: NonEmpty[Set[SigningKeyUsage]], @@ -268,7 +271,7 @@ class KmsPrivateCrypto( _ = privateStore.storeKeyMetadata(KmsMetadata(publicKey.id, keyId, KeyPurpose.Encryption)) } yield publicKey - override def decrypt[M](encrypted: AsymmetricEncrypted[M])( + override private[crypto] def decryptInternal[M](encrypted: AsymmetricEncrypted[M])( deserialize: ByteString => Either[DeserializationError, M] )(implicit tc: TraceContext): EitherT[FutureUnlessShutdown, DecryptionError, M] = for { @@ -324,6 +327,7 @@ object KmsPrivateCrypto { encryptionSchemes: EncryptionCryptoSchemes, cryptoPublicStore: CryptoPublicStore, kmsCryptoPrivateStore: KmsCryptoPrivateStore, + cryptoMetrics: CryptoMetrics, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, )(implicit executionContext: ExecutionContext): KmsPrivateCrypto = @@ -333,6 +337,8 @@ object KmsPrivateCrypto { cryptoPublicStore, signingSchemes, encryptionSchemes, + cryptoMetrics.signingMetrics, + cryptoMetrics.decryptionMetrics, timeouts, loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala index d0cf61e031..d04f9b4806 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSigner.scala @@ -24,7 +24,7 @@ import com.digitalasset.canton.crypto.{ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} -import com.digitalasset.canton.metrics.KmsMetrics +import com.digitalasset.canton.metrics.CryptoMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.sequencing.client.SequencerClientConfig import com.digitalasset.canton.time.Clock @@ -108,7 +108,7 @@ object SyncCryptoSigner { member: Member, crypto: SynchronizerCrypto, cryptoConfig: CryptoConfig, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, publicKeyConversionCacheConfig: CacheConfig, futureSupervisor: FutureSupervisor, timeouts: ProcessingTimeout, @@ -130,7 +130,7 @@ object SyncCryptoSigner { staticSynchronizerParameters, member, crypto.privateCrypto, - kmsMetrics, + cryptoMetrics, crypto.cryptoPrivateStore, cryptoConfig.sessionSigningKeys, publicKeyConversionCacheConfig, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala index 6e94d06a5b..c91a22adca 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/signer/SyncCryptoSignerWithSessionKeys.scala @@ -5,6 +5,8 @@ package com.digitalasset.canton.crypto.signer import cats.data.EitherT import cats.syntax.either.* +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.daml.nonempty.NonEmpty import com.digitalasset.canton.concurrent.{ExecutorServiceExtensions, FutureSupervisor, Threading} import com.digitalasset.canton.config.RequireTypes.PositiveInt @@ -31,7 +33,7 @@ import com.digitalasset.canton.lifecycle.{ UnlessShutdown, } import com.digitalasset.canton.logging.NamedLoggerFactory -import com.digitalasset.canton.metrics.KmsMetrics +import com.digitalasset.canton.metrics.{CryptoMetrics, SigningHistograms, SigningMetrics} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.{Member, SynchronizerId} @@ -66,7 +68,7 @@ class SyncCryptoSignerWithSessionKeys( staticSynchronizerParameters: StaticSynchronizerParameters, member: Member, signPrivateApiWithLongTermKeys: SigningPrivateOps, - kmsMetrics: Option[KmsMetrics], + cryptoMetrics: CryptoMetrics, override protected val cryptoPrivateStore: CryptoPrivateStore, sessionSigningKeysConfig: SessionSigningKeysConfig, publicKeyConversionCacheConfig: CacheConfig, @@ -106,6 +108,13 @@ class SyncCryptoSignerWithSessionKeys( // this `JcePureCrypto` object only holds private key conversions spawned from sign calls privateKeyConversionCacheTtl = Some(sessionSigningKeysConfig.keyEvictionPeriod.underlying), signatureVerificationParallelism = PositiveInt.one, // not used + signingMetrics = new SigningMetrics( + new SigningHistograms(MetricName("signing"))(new HistogramInventory()), + NoOpMetricsFactory, + )( + MetricsContext.Empty + ), // not used since we only want to record latency for KMS signing requests + decryptionMetrics = cryptoMetrics.decryptionMetrics, // not used loggerFactory = loggerFactory, ) @@ -548,7 +557,9 @@ class SyncCryptoSignerWithSessionKeys( _.validityPeriodEnd.contains(CantonTimestamp.MaxValue) ) ) - kmsMetrics.foreach(_.sessionSigningKeysFallback.inc()) + cryptoMetrics.kmsMetricsO.foreach(kmsMetrics => + kmsMetrics.sessionSigningKeysFallback.inc() + ) signPrivateApiWithLongTermKeys .sign(hash, activeLongTermKey.id, usage) .leftMap[SyncCryptoError](SyncCryptoError.SyncCryptoSigningError.apply) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala index 6308039807..540e39fcb5 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/crypto/verifier/SyncCryptoVerifier.scala @@ -6,6 +6,8 @@ package com.digitalasset.canton.crypto.verifier import cats.data.EitherT import cats.implicits.{catsSyntaxAlternativeSeparate, catsSyntaxValidatedId} import cats.syntax.either.* +import com.daml.metrics.api.noop.NoOpMetricsFactory +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricsContext} import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.CacheConfig import com.digitalasset.canton.config.RequireTypes.PositiveInt @@ -33,6 +35,12 @@ import com.digitalasset.canton.crypto.{ } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.metrics.{ + DecryptionHistograms, + DecryptionMetrics, + SigningHistograms, + SigningMetrics, +} import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.topology.client.TopologySnapshot import com.digitalasset.canton.topology.{Member, SynchronizerId} @@ -88,6 +96,14 @@ class SyncCryptoVerifier( // with a public signing key, and the private key conversion cache is never used. privateKeyConversionCacheTtl = None, signatureVerificationParallelism = signatureVerificationParallelism, + signingMetrics = new SigningMetrics( + new SigningHistograms(MetricName("signing"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), // not used + decryptionMetrics = new DecryptionMetrics( + new DecryptionHistograms(MetricName("decryption"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty), // not used loggerFactory = loggerFactory, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala new file mode 100644 index 0000000000..31db14708f --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/CryptoMetrics.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.metrics + +/** Aggregates all cryptographic-related metrics. + * + * @param signingMetrics + * Metrics for signing operations + * @param decryptionMetrics + * Metrics for decryption operations + * @param kmsMetricsO + * Optional metrics for KMS-backed operations; defined only when KMS is in use + */ +class CryptoMetrics( + val signingMetrics: SigningMetrics, + val decryptionMetrics: DecryptionMetrics, + val kmsMetricsO: Option[KmsMetrics], +) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala new file mode 100644 index 0000000000..8fe279b71b --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/DecryptionMetrics.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.metrics + +import com.daml.metrics.api.HistogramInventory.Item +import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Timer} +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricQualification, MetricsContext} + +class DecryptionHistograms(val parent: MetricName)(implicit + inventory: HistogramInventory +) { + + private[metrics] val prefix: MetricName = parent :+ "decryption" + + private[metrics] val decryptLatency: Item = Item( + prefix :+ "latency", + summary = "Latency of decryption requests.", + description = "Measures the latency of decryption operations.", + qualification = MetricQualification.Latency, + ) +} + +class DecryptionMetrics( + histograms: DecryptionHistograms, + labeledMetricsFactory: LabeledMetricsFactory, +)(implicit context: MetricsContext) { + + val prefix: MetricName = histograms.prefix + val decryptLatency: Timer = labeledMetricsFactory.timer(histograms.decryptLatency.info) +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala index c275e9c61e..3cdbd92113 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SequencerConnectionPoolMetrics.scala @@ -105,25 +105,19 @@ class SequencerConnectionPoolMetrics( connectionHealthMetrics.getOrElseUpdate(mc, Eval.later(createConnectionHealthGauge)).value } - def removeMetricsForAllConnections(): Unit = { - connectionHealthMetrics.values.foreach(_.value.close) - connectionHealthMetrics.clear() + def removeMetricsForAllConnections(psidO: Option[String]): Unit = + removeInternal(_.labels.get("psid") == psidO) - subscriptionHealthMetrics.values.foreach(_.value.close) - subscriptionHealthMetrics.clear() - } - - def removeMetricsForConnection(toRemove: Set[String]): Unit = { - // remove from map - val connsToRemove = connectionHealthMetrics.keys - .filter(_.labels.get("connection").exists(toRemove(_))) - connsToRemove.foreach(mc => connectionHealthMetrics.remove(mc).foreach(_.value.close())) + def removeMetricsForConnection(toRemove: Set[String], psidO: Option[String]): Unit = + removeInternal(mc => + mc.labels.get("psid") == psidO && mc.labels.get("connection").exists(toRemove(_)) + ) - val subsToRemove = subscriptionHealthMetrics.keys - .filter(_.labels.get("connection").exists(toRemove(_))) - subsToRemove - .foreach(mc => subscriptionHealthMetrics.remove(mc).foreach(_.value.close)) - } + private def removeInternal[T](filter: MetricsContext => Boolean): Unit = + Seq(connectionHealthMetrics, subscriptionHealthMetrics).foreach { metricsMap => + val toRemove = metricsMap.keys.filter(filter) + toRemove.foreach(metricsMap.remove(_).foreach(_.value.close)) + } // Gauges don't support metrics context per update. So instead create a map with a gauge per context. private val subscriptionHealthMetrics: TrieMap[MetricsContext, Eval[Gauge[Int]]] = TrieMap.empty diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala new file mode 100644 index 0000000000..476b145934 --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/metrics/SigningMetrics.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.metrics + +import com.daml.metrics.api.HistogramInventory.Item +import com.daml.metrics.api.MetricHandle.{LabeledMetricsFactory, Timer} +import com.daml.metrics.api.{HistogramInventory, MetricName, MetricQualification, MetricsContext} + +class SigningHistograms(val parent: MetricName)(implicit + inventory: HistogramInventory +) { + + private[metrics] val prefix: MetricName = parent :+ "signing" + + private[metrics] val signingLatency: Item = Item( + prefix :+ "latency", + summary = "Latency of signing requests.", + description = "Measures the latency of signing operations.", + qualification = MetricQualification.Latency, + ) +} + +class SigningMetrics( + histograms: SigningHistograms, + labeledMetricsFactory: LabeledMetricsFactory, +)(implicit context: MetricsContext) { + + val prefix: MetricName = histograms.prefix + val signingLatency: Timer = labeledMetricsFactory.timer(histograms.signingLatency.info) +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala index 8378d5b32c..53d3b509f6 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/networking/grpc/GrpcError.scala @@ -10,6 +10,7 @@ import com.digitalasset.canton.sequencing.authentication.MemberAuthentication.{ MissingToken, } import com.digitalasset.canton.sequencing.authentication.grpc.Constant +import com.digitalasset.canton.sequencing.protocol.SequencerErrors.AggregateSubmissionAlreadySent import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* import io.grpc.Status.Code.* @@ -122,6 +123,28 @@ object GrpcError { } } + /** The server rejected this request because it duplicates an existing resource. */ + final case class GrpcRequestRefusedAlreadyExists( + request: String, + serverName: String, + status: Status, + optTrailers: Option[Metadata], + decodedCantonError: Option[DecodedCantonError], + ) extends GrpcError { + + override protected def logFullCause: Boolean = true + + def isAuthenticationTokenMissing: Boolean = false + + override def log(logger: TracedLogger)(implicit traceContext: TraceContext): Unit = + // no need to log this dramatically as it is expected to happen + if (decodedCantonError.exists(_.code.id == AggregateSubmissionAlreadySent.id)) { + logger.info(s"""Request failed for $serverName.$hint + | ${getClass.getSimpleName}: ${status.getCode} / ${status.getDescription}""".mkString) + } else logger.info(toString) + + } + /** The client gave up waiting for a response. The server may or may not process the request. It * may or may not make sense to retry, depending on the specific situation. */ @@ -199,9 +222,12 @@ object GrpcError { else GrpcClientError(request, serverName, status, optTrailers, decodedError) case FAILED_PRECONDITION | NOT_FOUND | OUT_OF_RANGE | RESOURCE_EXHAUSTED | ABORTED | - PERMISSION_DENIED | ALREADY_EXISTS => + PERMISSION_DENIED => GrpcRequestRefusedByServer(request, serverName, status, optTrailers, decodedError) + case ALREADY_EXISTS => + GrpcRequestRefusedAlreadyExists(request, serverName, status, optTrailers, decodedError) + case DEADLINE_EXCEEDED | CANCELLED => GrpcClientGaveUp(request, serverName, status, optTrailers, decodedError) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala index 8e6c44c510..fc0aac13fb 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbMigrations.scala @@ -26,6 +26,7 @@ import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.retry.RetryEither import com.digitalasset.canton.util.{LoggerUtil, MonadUtil, ResourceUtil} import org.flywaydb.core.Flyway +import org.flywaydb.core.api.configuration.FluentConfiguration import org.flywaydb.core.api.{FlywayException, MigrationInfo} import slick.jdbc.JdbcBackend.Database import slick.jdbc.hikaricp.HikariCPJdbcDataSource @@ -35,6 +36,7 @@ import java.sql.SQLException import javax.sql.DataSource import scala.concurrent.duration.Duration import scala.concurrent.{ExecutionContext, blocking} +import scala.jdk.CollectionConverters.* /** Performs DB migrations using Flyway. * @@ -59,6 +61,9 @@ class DbMigrations( * https://flywaydb.org/documentation/getstarted/firststeps/api */ protected def createFlyway(dataSource: DataSource): Flyway = + createFlywayConfig(dataSource: DataSource).load() + + protected def createFlywayConfig(dataSource: DataSource): FluentConfiguration = Flyway.configure .locations(dbConfig.buildMigrationsPaths(alphaVersionSupport)*) .dataSource(dataSource) @@ -66,7 +71,11 @@ class DbMigrations( .cleanOnValidationError(dbConfig.parameters.unsafeCleanOnValidationError) .baselineOnMigrate(dbConfig.parameters.unsafeBaselineOnMigrate) .lockRetryCount(60) - .load() + .placeholders( + Map( + "initialBftOrdererTablesPartitionSize" -> dbConfig.parameters.partitions.initialBftOrdererTablesPartitionSize.toString + ).asJava + ) protected def withCreatedDb[A](retryConfig: DbStorage.RetryConfig)( fn: Database => EitherT[UnlessShutdown, DbMigrations.Error, A] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala index 3f63f46b7e..5950b788e1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageMulti.scala @@ -193,7 +193,7 @@ final class DbStorageMulti private ( // Run the initial health check checkHealth(clock.now) - private val writeDb: Database = DbLockedConnectionPool.createDatabaseFromPool( + private[canton] val writeDb: Database = DbLockedConnectionPool.createDatabaseFromPool( writeConnectionPool, writeDbExecutor, ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala index 9c98041df8..b296c83d8f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/resource/DbStorageSingle.scala @@ -38,7 +38,7 @@ import scala.concurrent.{ExecutionContext, Future, blocking} final class DbStorageSingle private ( override val profile: DbStorage.Profile, override val dbConfig: DbConfig, - db: Database, + private[canton] val db: Database, clock: Clock, override protected val logOperations: Boolean, override val metrics: DbStorageMetrics, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala index fa1d37af6b..8f5c78708c 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/PeriodicAcknowledgements.scala @@ -8,7 +8,12 @@ import com.daml.nameof.NameOf.functionFullName import com.digitalasset.canton.config.ProcessingTimeout import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps -import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown, UnlessShutdown} +import com.digitalasset.canton.lifecycle.{ + FlagCloseable, + FutureUnlessShutdown, + HasCloseContext, + UnlessShutdown, +} import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.store.SequencerCounterTrackerStore import com.digitalasset.canton.time.Clock @@ -45,7 +50,8 @@ class PeriodicAcknowledgements( )(implicit executionContext: ExecutionContext) extends NamedLogging with FlagCloseable - with HasFlushFuture { + with HasFlushFuture + with HasCloseContext { private val priorAckRef = new AtomicReference[Option[CantonTimestamp]](None) @@ -94,7 +100,7 @@ class PeriodicAcknowledgements( withNewTraceContext("schedule_next_periodic_ack") { implicit traceContext => synchronizeWithClosingSync(functionFullName)( clock - .scheduleAfter( + .scheduleAfterCancelledOnShutdown( { _ => // Schedule the next update as soon as possible after the interval has passed; // for static time tests, this runs synchronously when the time is advanced @@ -102,6 +108,7 @@ class PeriodicAcknowledgements( scheduleNextUpdate() // Async-trampolined update() }, + "periodick-ack", interval.toJava, ) .discard[FutureUnlessShutdown[Unit]] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala index d5d353dfd2..5dd311e907 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClient.scala @@ -60,6 +60,7 @@ import com.digitalasset.canton.sequencing.client.SendTracker.{LatestAttempt, Lat import com.digitalasset.canton.sequencing.client.SequencerClient.{ ConnectionContainer, SequencerTransports, + TrafficCostValidator, } import com.digitalasset.canton.sequencing.client.SequencerClientImpl.SequencerClientTimeSourcesPool import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps @@ -121,7 +122,7 @@ import org.apache.pekko.{Done, NotUsed} import org.slf4j.event.Level import java.util.concurrent.atomic.AtomicReference -import scala.annotation.nowarn +import scala.annotation.{nowarn, unused} import scala.compat.java8.DurationConverters.FiniteDurationops import scala.concurrent.* import scala.concurrent.duration.* @@ -300,6 +301,7 @@ abstract class SequencerClientImpl( messageId: MessageId, aggregationRule: Option[AggregationRule], callback: SendCallback, + trafficCostValidator: TrafficCostValidator, amplify: Boolean, useConfirmationResponseAmplificationParameters: Boolean, )(implicit @@ -312,6 +314,7 @@ abstract class SequencerClientImpl( messageId, aggregationRule, callback, + trafficCostValidator, amplify, useConfirmationResponseAmplificationParameters, metricsContext, @@ -339,6 +342,7 @@ abstract class SequencerClientImpl( messageId: MessageId, aggregationRule: Option[AggregationRule], callback: SendCallback, + trafficCostValidator: TrafficCostValidator, amplify: Boolean, useConfirmationResponseAmplificationParameters: Boolean, metricsContext: MetricsContext, @@ -477,6 +481,9 @@ abstract class SequencerClientImpl( .checkSenderAndRecipientsAreRegistered(request, snapshot) .leftMap(_.toSendAsyncClientError) acceptableSequencersO <- EitherT.right(getAcceptableSequencers(snapshot)) + _ <- EitherT.liftF( + cost.parTraverse_(c => trafficCostValidator.validate(c.cost.unwrap, traceContext)) + ) latestAttemptRef <- EitherT.fromEither[FutureUnlessShutdown](trackSend) _ = recorderO.foreach(_.recordSubmission(request)) res <- performSend( @@ -1522,7 +1529,7 @@ class RichSequencerClientImpl( sequencerSubscriptionFactory, subscriptionHandlerFactory, metrics.connectionPool, - metricsContext = MetricsContext.Empty, + connectionPool.metricsContext, timeouts, loggerFactory, ) @@ -2340,4 +2347,25 @@ object SequencerClient { sequencerId: SequencerId, ): NamedLoggerFactory = loggerFactory.append("sequencerId", sequencerId.uid.toString) + + trait TrafficCostValidator { + + /** Validates that the traffic cost is valid in the context of the submitting member and the + * current submission request. + * + * Practically, this is relevant for requests from submitting participants that perform traffic + * enforcement against local user traffic accounts. + */ + def validate(trafficCost: Long, traceContext: TraceContext): FutureUnlessShutdown[Unit] + } + + object TrafficCostValidator { + val NoTrafficCostValidation: TrafficCostValidator = new TrafficCostValidator { + override def validate( + @unused trafficCost: Long, + @unused traceContext: TraceContext, + ): FutureUnlessShutdown[Unit] = + FutureUnlessShutdown.unit + } + } } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala index b17dad0867..85ea9e96f5 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientFactory.scala @@ -243,11 +243,13 @@ object SequencerClientFactory { // Make a BFT call to all the transports to retrieve the current traffic state from the synchronizer // and initialize the trafficStateController with it trafficInitTimestampO = latestSequencedTimestampO - .orElse( - synchronizerPredecessor.map( - _.upgradeTime - ) - ) + .orElse(synchronizerPredecessor.map(_.upgradeTime)) + /* + Mediator nodes don't expose traffic. + This also prevent them from connecting to the sequencer during LSU before upgrade time, which + is needed for the test sequencing messages. + */ + .filter(_ => member.code != MediatorId.Code) _ = logger.info( s"Initializing traffic state at timestamp: $trafficInitTimestampO" diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala index 9a0f9b95a6..dab9680511 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/SequencerClientSend.scala @@ -8,6 +8,8 @@ import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator.NoTrafficCostValidation import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.protocol.{AggregationRule, Batch, MessageId} import com.digitalasset.canton.time.Clock @@ -71,6 +73,10 @@ trait SequencerClientSend { * independent of the configuration. * @param timestamps * Aggregated timestamps needed for sending a request. + * @param trafficCostValidator + * Validate the traffic cost of the submission batch against the local traffic state of the + * member, only if traffic control is enabled for the sequencer. If validation fails, the + * submission request is not sent to the sequencer and an error is returned. */ def sendAsync( batch: Batch[DefaultOpenEnvelope], @@ -78,6 +84,7 @@ trait SequencerClientSend { messageId: MessageId = generateMessageId, aggregationRule: Option[AggregationRule] = None, callback: SendCallback = SendCallback.empty, + trafficCostValidator: TrafficCostValidator, amplify: Boolean = false, useConfirmationResponseAmplificationParameters: Boolean = false, )(implicit @@ -95,6 +102,7 @@ trait SequencerClientSend { messageId: MessageId = generateMessageId, aggregationRule: Option[AggregationRule] = None, callback: SendCallback = SendCallback.empty, + trafficCostValidator: TrafficCostValidator = NoTrafficCostValidation, amplify: Boolean = false, useConfirmationResponseAmplificationParameters: Boolean = false, )(implicit @@ -106,6 +114,7 @@ trait SequencerClientSend { messageId = messageId, aggregationRule = aggregationRule, callback = callback, + trafficCostValidator = trafficCostValidator, amplify = amplify, useConfirmationResponseAmplificationParameters = useConfirmationResponseAmplificationParameters, ).value.flatMap(identity) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala index 4723d6c8eb..8b21366659 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcInternalSequencerConnection.scala @@ -66,7 +66,7 @@ class GrpcInternalSequencerConnection private[sequencing] ( private val connection: GrpcConnection = GrpcConnection(config, params, metrics, timeouts, loggerFactory) - private val connectionMetricsContext: MetricsContext = metricsContext.withExtraLabels( + private implicit val connectionMetricsContext: MetricsContext = metricsContext.withExtraLabels( "connection" -> connection.config.name ) private val stub: SequencerConnectionStub = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala index 4f097360bb..cfe4e9138b 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcSequencerConnection.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.networking.grpc.GrpcError.{ GrpcClientError, + GrpcRequestRefusedAlreadyExists, GrpcRequestRefusedByServer, GrpcServiceUnavailable, } @@ -134,10 +135,17 @@ class GrpcSequencerConnection( // Adapted from GrpcSequencerClientTransportCommon Either.cond( !bubbleSendErrorPolicy(error), { - // log that we're swallowing the error - logger.info( - s"Send [$messageId] returned an error however may still be possibly sequenced so we are ignoring the error: $error" - ) + error match { + // TODO(#12377) Do not trust the sequencer but monitor and stop using the given sequencer if it is denying service + case ConnectionError.TransportError(error: GrpcRequestRefusedAlreadyExists) => + // already logged as an info in GrpcConnection + case _ => + // log that we're swallowing the error + logger.info( + s"Send [$messageId] returned an error however may still be possibly sequenced so we are ignoring the error: $error" + ) + } + () }, error match { @@ -172,6 +180,8 @@ class GrpcSequencerConnection( case _: GrpcError.GrpcClientError => true // the request was rejected by the server as it wasn't in a state to accept it case _: GrpcError.GrpcRequestRefusedByServer => true + // the request was rejected by the server because it already exists, so we don't need to bubble up + case _: GrpcError.GrpcRequestRefusedAlreadyExists => false // an internal error happened at the server, this could have been when constructing or sending the response // after accepting the request so we cannot safely bubble the error case _: GrpcError.GrpcServerError => false diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala index 317330bbf4..0ea28583ea 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/GrpcUserSequencerConnectionStub.scala @@ -91,9 +91,8 @@ class GrpcUserSequencerConnectionStub( SendAsyncRequest(signedSubmissionRequest = request.toByteString) ) ) - .leftMap( - SequencerConnectionStubError.ConnectionError.apply - ) + .leftMap(SequencerConnectionStubError.ConnectionError.apply) + } yield () } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala index 88ad976667..33771788b8 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPool.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.sequencing.client.pool import cats.data.EitherT import cats.syntax.either.* import com.daml.grpc.adapter.ExecutionSequencerFactory +import com.daml.metrics.api.MetricsContext import com.daml.nonempty.NonEmpty import com.digitalasset.canton.SequencerAlias import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveInt} @@ -145,6 +146,9 @@ trait SequencerConnectionPool extends FlagCloseable with NamedLogging { /** Obtain all the sequencer IDs present in the pool, associated to their sequencer alias */ def getAllSequencerIds(implicit traceContext: TraceContext): Map[SequencerAlias, SequencerId] + /** The context for the pool metrics */ + def metricsContext: MetricsContext + /** Determine whether the connection pool can still reach the given threshold, ignoring the * `ignored` connections and considering an additional `extraUndecided` number of undecided * connections. diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala index 4d19be7be2..1e4dfa60b9 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImpl.scala @@ -64,7 +64,7 @@ class SequencerConnectionPoolImpl private[sequencing] ( crypto: Crypto, seedForRandomnessO: Option[Long], metrics: SequencerConnectionPoolMetrics, - metricsContext: MetricsContext, + override val metricsContext: MetricsContext, futureSupervisor: FutureSupervisor, override protected val timeouts: ProcessingTimeout, override protected val loggerFactory: NamedLoggerFactory, @@ -138,7 +138,8 @@ class SequencerConnectionPoolImpl private[sequencing] ( override def staticSynchronizerParametersO: Option[StaticSynchronizerParameters] = bootstrapCell.get.map(_.staticParameters) - private implicit def mc: MetricsContext = metricsContext + private implicit val mc: MetricsContext = metricsContext + metrics.trustThreshold.updateValue(config.trustThreshold.value) override def start()(implicit @@ -153,7 +154,6 @@ class SequencerConnectionPoolImpl private[sequencing] ( updateTrackedConnections( toBeAdded = config.connections, toBeRemoved = Set.empty, - isInitialUpdate = true, ) } @@ -236,7 +236,6 @@ class SequencerConnectionPoolImpl private[sequencing] ( private def updateTrackedConnections( toBeAdded: immutable.Iterable[ConnectionConfig], toBeRemoved: Set[ConnectionConfig], - isInitialUpdate: Boolean, )(implicit traceContext: TraceContext): Unit = lock.exclusive { val removedConnections = @@ -260,11 +259,8 @@ class SequencerConnectionPoolImpl private[sequencing] ( removedConnections.foreach { connection => connection.fatal("Removed from configuration") } - if (isInitialUpdate) { - metrics.removeMetricsForAllConnections() - } else { - metrics.removeMetricsForConnection(toBeRemoved.map(_.name)) - } + + metrics.removeMetricsForConnection(toBeRemoved.map(_.name), mc.labels.get("psid")) // If start() or updateConfig() is called after the pool has been closed, we don't want to start new connections if (!isClosing) { @@ -464,7 +460,6 @@ class SequencerConnectionPoolImpl private[sequencing] ( updateTrackedConnections( toBeAdded = changedConnections.added, toBeRemoved = changedConnections.removed, - isInitialUpdate = false, ) } } @@ -573,6 +568,7 @@ class SequencerConnectionPoolImpl private[sequencing] ( // We close the connections outside the critical section to avoid shutdown problems in case // it triggers health callbacks LifeCycle.close(instances*)(logger) + metrics.removeMetricsForAllConnections(mc.labels.get("psid")) super.onClosed() } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala index 9be2ed91f7..d003bd97d1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/pool/SequencerSubscriptionPoolImpl.scala @@ -86,7 +86,8 @@ final class SequencerSubscriptionPoolImpl private[sequencing] ( private def currentConfigWithThreshold: ConfigWithThreshold = ConfigWithThreshold(config, pool.config.trustThreshold) - private implicit def mc: MetricsContext = metricsContext + private implicit val mc: MetricsContext = metricsContext + metrics.subscriptionThreshold.updateValue( currentConfigWithThreshold.activeThreshold.value ) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala index 09378fccfe..53541d302d 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/client/transports/GrpcSubscriptionErrorRetryPolicy.scala @@ -50,6 +50,9 @@ object GrpcSubscriptionErrorRetryPolicy { loggingContext.debug("Not trying to reconnect.") retry + case error: GrpcError.GrpcRequestRefusedAlreadyExists => + false // not retrying + case _: GrpcError.GrpcServerError => // We believe these errors (INTERNAL, UNKNOWN, DATA_LOSS) can in some circumstances by transient, and // therefore we err on the side of caution and retry. diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala index 5c0a307cdf..9286bbd00f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedEnvelope.scala @@ -60,6 +60,20 @@ object ClosedEnvelope { snapshot.verifySignatures(hash, sender, signatures, SigningKeyUsage.ProtocolOnly) } + def verifyKeyUsage( + snapshot: SyncCryptoApi, + sender: Member, + signature: Signature, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, SignatureCheckError, Unit] = + snapshot.verifyKeyUsage( + sender, + signature.authorizingLongTermKey, + signature.signatureDelegation, + SigningKeyUsage.ProtocolOnly, + ) + def verifyMediatorSignatures( snapshot: SyncCryptoApi, mediatorGroupIndex: MediatorGroupIndex, diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala index 60a0b7cb85..817b1c8c82 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/ClosedUncompressedEnvelope.scala @@ -26,7 +26,7 @@ import com.digitalasset.canton.serialization.ProtoConverter import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult import com.digitalasset.canton.topology.Member import com.digitalasset.canton.tracing.TraceContext -import com.digitalasset.canton.util.{ByteStringUtil, MaxBytesToDecompress} +import com.digitalasset.canton.util.{ByteStringUtil, MaxBytesToDecompress, MonadUtil} import com.digitalasset.canton.version.{ HasProtocolVersionedWrapper, ProtoVersion, @@ -171,6 +171,15 @@ final case class ClosedUncompressedEnvelope private[protocol] ( .from(signatures) .traverse_(ClosedEnvelope.verifySignatures(snapshot, sender, bytes, _)) + def verifyKeyUsage( + snapshot: SyncCryptoApi, + sender: Member, + )(implicit + ec: ExecutionContext, + traceContext: TraceContext, + ): EitherT[FutureUnlessShutdown, SignatureCheckError, Unit] = + MonadUtil.sequentialTraverse_(signatures)(ClosedEnvelope.verifyKeyUsage(snapshot, sender, _)) + @VisibleForTesting override def withRecipients(newRecipients: Recipients): ClosedUncompressedEnvelope = copy(recipients = newRecipients) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala index 196aa7052b..c2d4473de3 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SendAsyncError.scala @@ -24,6 +24,7 @@ sealed trait SendAsyncError extends PrettyPrinting { /** The max sequencing time has elapsed and the request was refused */ def hasMaxSequencingTimeElapsed: Boolean + } object SendAsyncError { @@ -56,6 +57,7 @@ object SendAsyncError { } case _ => false } + } /** Implementation of [[SendAsyncError]]s for direct transports */ @@ -67,5 +69,6 @@ object SendAsyncError { // Only used for amplification, but direct sequencer transport doesn't use amplification override def isMaxSequencingTimeTooFar: Boolean = false + } } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala index 17c30276a0..0c624940c1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SequencerDeliverError.scala @@ -17,6 +17,7 @@ import com.digitalasset.canton.error.CantonErrorGroups.SequencerErrorGroup import com.digitalasset.canton.error.{CantonBaseError, TransactionError, TransactionErrorImpl} import com.digitalasset.canton.networking.grpc.GrpcError import com.digitalasset.canton.topology.Member +import com.digitalasset.canton.version.ProtocolVersion import com.google.rpc.status.Status import java.time.Instant @@ -202,6 +203,28 @@ object SequencerErrors extends SequencerErrorGroup { extends SequencerDeliverErrorCode( id = "SEQUENCER_AGGREGATE_SUBMISSION_ALREADY_SENT", ErrorCategory.InvalidGivenCurrentSystemStateOther, + ) { + + def apply(message: String, protocolVersion: ProtocolVersion): SequencerDeliverError = + // for backward compatibility reasons, we need to keep the message format unchanged + if (protocolVersion <= ProtocolVersion.v35) + super.apply(message) + else + AggregateSubmissionAlreadySentV2.apply(message) + } + + @Explanation( + """This error occurs when the sequencer has already sent out the aggregate submission for the request.""" + ) + @Resolution( + """This is expected to happen during operation of a system with aggregate submissions enabled. No action required. + |This error code has been modified to report Grpc RESOURCE_EXISTS and is used for synchronous rejects and + |async rejects starting with PV36.""" + ) + case object AggregateSubmissionAlreadySentV2 + extends SequencerDeliverErrorCode( + id = "SEQUENCER_AGGREGATE_SUBMISSION_ALREADY_SENT", + ErrorCategory.InvalidGivenCurrentSystemStateResourceExists, ) @Explanation( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala index 1a755c6baa..dac698fc19 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/sequencing/protocol/SignedContent.scala @@ -84,6 +84,19 @@ final case class SignedContent[+A <: HasCryptographicEvidence] private ( snapshot.verifySignature(hash, member, signature, SigningKeyUsage.ProtocolOnly) } + def verifyKeyUsage( + snapshot: SyncCryptoApi, + member: Member, + )(implicit + traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, SignatureCheckError, Unit] = + snapshot.verifyKeyUsage( + member, + signature.authorizingLongTermKey, + signature.signatureDelegation, + SigningKeyUsage.ProtocolOnly, + ) + def deserializeContent[B <: HasCryptographicEvidence]( contentDeserializer: ByteString => ParsingResult[B] ): ParsingResult[SignedContent[B]] = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala index ee92f296b6..abfcaae62f 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/store/Purgeable.scala @@ -23,6 +23,8 @@ trait Purgeable { trait ChunkPurgeable { self: FlagCloseable => + def name: String + /** Deletes a chunk of items from this store. No guarantees are made around transactionality, nor * about which specific items are deleted. * diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala index 3c1e027b6d..7a6fecc6b0 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/time/Clock.scala @@ -12,11 +12,15 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.CantonError import com.digitalasset.canton.error.CantonErrorGroups.ClockErrorGroup +import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.* import com.digitalasset.canton.lifecycle.UnlessShutdown.AbortedDueToShutdown import com.digitalasset.canton.lifecycle.{ + CloseContext, FlagCloseable, FutureUnlessShutdown, LifeCycle, + LifeCycleRegistrationHandle, + RunOnClosing, SyncCloseable, UnlessShutdown, } @@ -42,6 +46,7 @@ import com.digitalasset.canton.topology.admin.v30.{ } import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.ShowUtil.* +import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.retry.{AllExceptionRetryPolicy, Pause} import com.digitalasset.canton.util.{ErrorUtil, PriorityBlockingQueueUtil} import com.google.common.annotations.VisibleForTesting @@ -52,7 +57,7 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import java.util.concurrent.{Callable, PriorityBlockingQueue, TimeUnit} import scala.annotation.tailrec import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContextExecutor, Promise} +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Promise} import scala.util.Try /** A clock returning the current time, but with a twist: it always returns unique timestamps. If @@ -271,6 +276,70 @@ abstract class Clock() extends TimeProvider with AutoCloseable with NamedLogging } } + /** Schedule an action to be performed at the given timestamp. If the closeContext is closed, then + * the task is cancelled. + * + * If the provided timestamp is before `now`, the action skips queueing and is executed + * immediately. + * + * @param action + * action to run at the given timestamp (passing in the timestamp for when the task was + * scheduled) + * @param taskName + * name of the task + * @param timestamp + * timestamp when to run the task + * @return + * a future for the given task + */ + def scheduleAtCancelledOnShutdown[A]( + action: CantonTimestamp => A, + taskName: String, + timestamp: CantonTimestamp, + )(implicit ec: ExecutionContext, closeContext: CloseContext): FutureUnlessShutdown[A] = { + + val (f, handle) = scheduleAtCancellable(action, timestamp) + + val cancelTask = new RunOnClosing { + override def name: String = s"cancel-$taskName" + override def done: Boolean = f.isCompleted + override def run()(implicit traceContext: TraceContext): Unit = + handle.cancel(AbortedDueToShutdown) + } + + // Cancel the task upon shutdown + val lifeCycleRegistrationHandleUS: UnlessShutdown[LifeCycleRegistrationHandle] = + closeContext.context.runOnClose(cancelTask) + + f.thereafter { _ => + // Remove the lifeCycleRegistrationHandle when the task is finished + lifeCycleRegistrationHandleUS.foreach(_.cancel().discard) + } + } + + /** Schedule an action to be performed at the given timestamp. If the closeContext is closed, then + * the task is cancelled. + * + * If the provided timestamp is before `now`, the action skips queueing and is executed + * immediately. + * + * @param action + * action to run at the given timestamp (passing in the timestamp for when the task was + * scheduled) + * @param taskName + * name of the task + * @param delta + * duration to wait before running the task + * @return + * a future for the given task + */ + def scheduleAfterCancelledOnShutdown[A]( + action: CantonTimestamp => A, + taskName: String, + delta: Duration, + )(implicit ec: ExecutionContext, closeContext: CloseContext): FutureUnlessShutdown[A] = + scheduleAtCancelledOnShutdown(action, taskName, now.add(delta)) + // flush the task queue, stopping once we hit a task in the future @tailrec private def doFlush(): Option[CantonTimestamp] = { @@ -487,9 +556,8 @@ class SimClock( override def close(): Unit = {} - override protected def addToQueue(queue: Queued[?]): Unit = { - val _ = tasks.add(queue) - } + override protected def addToQueue(queue: Queued[?]): Unit = + tasks.add(queue).discard def reset(): Unit = { failTasks() diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala index ff93efb981..ec2951b9b1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/time/SynchronizerTimeTracker.scala @@ -15,6 +15,7 @@ import com.digitalasset.canton.discard.Implicits.* import com.digitalasset.canton.lifecycle.{ FlagCloseable, FutureUnlessShutdown, + HasCloseContext, LifeCycle, UnlessShutdown, } @@ -71,7 +72,8 @@ class SynchronizerTimeTracker( )(implicit executionContext: ExecutionContext) extends NamedLogging with FlagCloseable - with HasFlushFuture { + with HasFlushFuture + with HasCloseContext { /** Timestamps that we are waiting to observe held in ascending order. Queue access must be made * while holding the [[lock]]. @@ -517,7 +519,14 @@ class SynchronizerTimeTracker( val latestTimestamp = timestampRef.get().latest.fold(clock.now)(_.receivedAt) val expectUpdateBy = latestTimestamp.add(minObservationDuration).immediateSuccessor - val _ = clock.scheduleAt(performUpdate, expectUpdateBy) + clock + .scheduleAtCancelledOnShutdown( + action = performUpdate, + taskName = "min-observation", + expectUpdateBy, + ) + .discard + }.onShutdown(()) scheduleNextUpdate() diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala index 0ed122456c..440e66ec1a 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/ConfiguredPhysicalSynchronizerId.scala @@ -22,6 +22,7 @@ final case class KnownPhysicalSynchronizerId(psid: PhysicalSynchronizerId) override def toOption: Option[PhysicalSynchronizerId] = Some(psid) } + case object UnknownPhysicalSynchronizerId extends ConfiguredPhysicalSynchronizerId { override protected def pretty: Pretty[UnknownPhysicalSynchronizerId.type] = prettyOfString(_ => "UnknownPhysicalSynchronizerId") diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala index 4499f4332f..4541401f27 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/TopologyManager.scala @@ -451,6 +451,8 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC * the mapping that should be added * @param signingKeys * the keys which should be used to sign + * @param namespacesToSignFor + * the namespaces for which to sign * @param protocolVersion * the protocol version corresponding to the transaction * @param expectFullAuthorization @@ -466,6 +468,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC mapping: TopologyMapping, serial: Option[PositiveInt], signingKeys: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], protocolVersion: ProtocolVersion, expectFullAuthorization: Boolean, forceChanges: ForceFlags = ForceFlags.none, @@ -490,6 +493,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC signedTx <- signTransaction( tx, signingKeys, + namespacesToSignFor, isProposal = !expectFullAuthorization, protocolVersion, existingTransaction, @@ -553,7 +557,12 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC .Failure(transactionHash, effective, tooManyActiveTransactionsWithSameHash) ) }) - extendedTransaction <- extendSignature(existingTransaction, signingKeys, forceChanges) + extendedTransaction <- extendSignature( + existingTransaction, + signingKeys, + namespacesToSignFor = Seq.empty, + forceChanges, + ) _ <- add( Seq(extendedTransaction), expectFullAuthorization = expectFullAuthorization, @@ -631,6 +640,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC private def signTransaction[Op <: TopologyChangeOp, M <: TopologyMapping]( transaction: TopologyTransaction[Op, M], signingKeys: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], isProposal: Boolean, protocolVersion: ProtocolVersion, existingTransaction: Option[GenericSignedTopologyTransaction], @@ -644,7 +654,12 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC val transactionMapping = transaction.mapping for { // find signing keys. - keysToUseForSigning <- determineKeysToUse(transaction, signingKeys, forceChanges) + keysToUseForSigning <- determineKeysToUse( + transaction, + signingKeys, + namespacesToSignFor, + forceChanges, + ) // If the same operation and mapping is proposed repeatedly, insist that // new keys are being added. Otherwise, reject consistently with daml 2.x-based topology management. _ <- existingTransactionTuple match { @@ -678,13 +693,19 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC def extendSignature[Op <: TopologyChangeOp, M <: TopologyMapping]( transaction: SignedTopologyTransaction[Op, M], signingKeys: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], forceFlags: ForceFlags, )(implicit traceContext: TraceContext ): EitherT[FutureUnlessShutdown, TopologyManagerError, SignedTopologyTransaction[Op, M]] = for { // find signing keys - keys <- determineKeysToUse(transaction.transaction, signingKeys, forceFlags) + keys <- determineKeysToUse( + transaction.transaction, + signingKeys, + namespacesToSignFor, + forceFlags, + ) keysWithNoExistingSignature = keys.diff(transaction.signatures.map(_.authorizingLongTermKey)) updatedSignedTransaction <- NonEmpty.from(keysWithNoExistingSignature) match { case Some(keysWithNoExistingSignatureNE) => @@ -712,6 +733,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC private def determineKeysToUse( transaction: GenericTopologyTransaction, signingKeysToUse: Seq[Fingerprint], + namespacesToSignFor: Seq[Namespace], forceFlags: ForceFlags, )(implicit traceContext: TraceContext @@ -723,6 +745,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC for { requiredAuthAndUsableKeys <- loadValidSigningKeys( transaction, + namespacesToSignFor, returnAllValidKeys = true, ) (requiredAuth, usableKeys) = requiredAuthAndUsableKeys @@ -750,6 +773,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC for { requiredAuthAndDetectedKeysToUse <- loadValidSigningKeys( transaction, + namespacesToSignFor, returnAllValidKeys = false, ) (requiredAuth, detectedKeysToUse) = requiredAuthAndDetectedKeysToUse @@ -764,6 +788,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC private def loadValidSigningKeys( transaction: GenericTopologyTransaction, + namespacesToSignFor: Seq[Namespace], returnAllValidKeys: Boolean, )(implicit traceContext: TraceContext) = for { @@ -785,6 +810,7 @@ abstract class TopologyManager[+StoreID <: TopologyStoreId, +CryptoType <: BaseC ts, transaction, existing.headOption.map(_.transaction), // there should be at most one entry + namespacesToSignFor, returnAllValidKeys, ) } yield result diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala index 927a6f1cb7..51117d8c99 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetection.scala @@ -114,6 +114,8 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( * the topology transaction to sign * @param inStore * the latest fully authorized topology transaction with the same unique key as `toSign` + * @param namespacesToSignFor + * if non empty, only keys for the specified namespaces are returned * @param returnAllValidKeys * if true, returns all keys that can be used to sign. if false, only returns the most specific * keys per namespace/uid. @@ -124,6 +126,7 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( asOfExclusive: CantonTimestamp, toSign: GenericTopologyTransaction, inStore: Option[GenericTopologyTransaction], + namespacesToSignFor: Seq[Namespace], returnAllValidKeys: Boolean, )(implicit traceContext: TraceContext @@ -142,12 +145,17 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( relaxChecksForBackwardsCompatibility = false, ) ) + requestedAuthScope = Option.when(namespacesToSignFor.nonEmpty)( + ReferencedAuthorizations(namespaces = namespacesToSignFor.toSet) + ) - referencedAuth = requiredAuthFor( - toSign, - inStore, - relaxChecksForBackwardsCompatibility = false, - ).referenced + referencedAuth = requestedAuthScope.getOrElse( + requiredAuthFor( + toSign, + inStore, + relaxChecksForBackwardsCompatibility = false, + ).referenced + ) knownNsKeys = referencedAuth.namespaces.toSeq .parFlatTraverse(namespace => @@ -169,7 +177,8 @@ class TopologyManagerSigningKeyDetection[+PureCrypto <: CryptoPureApi]( selfSigned = EitherT.rightT[FutureUnlessShutdown, CryptoPrivateStoreError]( toSign.mapping match { case nsd @ NamespaceDelegation(ns, target, _) - if ns.fingerprint == target.fingerprint && nsd.canSign(Code.NamespaceDelegation) => + if ns.fingerprint == target.fingerprint && nsd + .canSign(Code.NamespaceDelegation) && referencedAuth.namespaces.contains(ns) => Seq(target.fingerprint) case _ => Seq.empty } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala index 2c8389f429..fa254d2012 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/TopologyStore.scala @@ -260,6 +260,8 @@ abstract class TopologyStore[+StoreID <: TopologyStoreId](implicit def predecessor: Option[SynchronizerPredecessor] + def name: String = s"topology-store ($storeId)" + /** fetch the effective time updates greater than or equal to a certain timestamp * * this function is used to recover the future effective timestamp such that we can reschedule @@ -335,6 +337,33 @@ abstract class TopologyStore[+StoreID <: TopologyStoreId](implicit traceContext: TraceContext ): FutureUnlessShutdown[PositiveStoredTopologyTransactions] + /** Returns the upgrade time of the LSU that lead to current synchronizer. + */ + def findUpgradeTimeFromPredecessor()(implicit + ev: StoreID <:< SynchronizerStore, + traceContext: TraceContext, + ): FutureUnlessShutdown[ + Option[CantonTimestamp] + ] = { + val currentPsid = ev(storeId).psid + + inspect( + proposals = false, + timeQuery = TimeQuery.Range(None, None), + asOfExclusiveO = None, + op = Some(TopologyChangeOp.Replace), + types = Seq(TopologyMapping.Code.LsuAnnouncement), + idFilter = Some(currentPsid.identifier.toProtoPrimitive), + namespaceFilter = Some(currentPsid.namespace.toProtoPrimitive), + ).map( + _.collectOfMapping[LsuAnnouncement] + .filter(_.mapping.successor.psid == currentPsid) + .result + .maxByOption(_.serial) + .map(_.mapping.upgradeTime) + ) + } + /** Same as [[findPositiveTransactions]] but returns negative transactions (with a remove * operation) */ @@ -608,7 +637,14 @@ abstract class TopologyStore[+StoreID <: TopologyStoreId](implicit .map(_.psid)}]" ) } else if (ongoingCopyFromPredecessor.compareAndSet(None, newRef)) { - val work = doCopyFromPredecessorSynchronizerStore(sourceStore) + errorLoggingContext.info( + s"LSU: About to copy topology from ${sourceStore.storeId.psid.suffix}" + ) + val work = doCopyFromPredecessorSynchronizerStore(sourceStore).map { _ => + errorLoggingContext.info( + s"LSU: Done copying topology from ${sourceStore.storeId.psid.suffix}" + ) + } newPromise .completeWithUS( work.thereafter(_ => ongoingCopyFromPredecessor.compareAndSet(newRef, None).discard) diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala index 54968c3005..f054e17d07 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/store/db/DbTopologyStore.scala @@ -1455,8 +1455,8 @@ class DbTopologyStore[+StoreId <: TopologyStoreId]( } } yield { logger.info( - if (deleted > 0) s"Deleted chunk of $deleted from topology store $storeId." - else s"No chunk to delete from topology store $storeId." + if (deleted > 0) s"Deleted chunk of $deleted from topology store" + else s"No chunk to delete from topology store." ) deleted > 0 } diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala index 0dfbd2b545..73510a8468 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/topology/transaction/TopologyMapping.scala @@ -236,7 +236,7 @@ object TopologyMapping { } - // Small wrapper to not have to work with a tuple3 (Set[Namespace], Set[Uid], Set[Fingerprint]) + // Small wrapper to not have to work with a 2-tuple (Set[Namespace], Set[Fingerprint]) final case class ReferencedAuthorizations( namespaces: Set[Namespace] = Set.empty, extraKeys: Set[Fingerprint] = Set.empty, @@ -1169,14 +1169,14 @@ object SynchronizerTrustCertificate extends TopologyMappingCompanion { /** When this feature flag is enabled, the participant will allow to reassign contracts between * synchronizers. This feature is in alpha and should not be used in production. */ - val EnableAlphaMultiSynchronizer: ParticipantTopologyFeatureFlag = + val EnableMultiSynchronizer: ParticipantTopologyFeatureFlag = ParticipantTopologyFeatureFlag( - v30.Enums.ParticipantFeatureFlag.PARTICIPANT_FEATURE_FLAG_ENABLE_ALPHA_MULTI_SYNCHRONIZER.value - )(Some("EnableAlphaMultiSynchronizer")) + v30.Enums.ParticipantFeatureFlag.PARTICIPANT_FEATURE_FLAG_ENABLE_MULTI_SYNCHRONIZER.value + )(Some("EnableMultiSynchronizer")) val knownTopologyFeatureFlags: Seq[ParticipantTopologyFeatureFlag] = Seq( ExternalSigningLocalContractsInSubview, - EnableAlphaMultiSynchronizer, + EnableMultiSynchronizer, ) def fromProtoV30( diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala index 8d0502fb42..719b37e229 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/PekkoUtil.scala @@ -1144,6 +1144,8 @@ object PekkoUtil extends HasLoggerName { def apply(index: Long): Unit } + type ShutdownInProgress = () => Boolean + final case class FutureQueueConsumer[T]( futureQueue: FutureQueue[(Long, T)], fromExclusive: Long, @@ -1193,8 +1195,7 @@ object PekkoUtil extends HasLoggerName { retryAttemptErrorThreshold: Int, uncommittedWarnTreshold: Int, recoveringQueueMetrics: RecoveringQueueMetrics, - consumerFactory: Commit => Future[FutureQueueConsumer[T]], - initializationKillSwitch: Option[() => Unit], + consumerFactory: Commit => ShutdownInProgress => Future[Future[FutureQueueConsumer[T]]], ) extends RecoveringFutureQueue[T] { assert(maxBlockedOffer > 0) assert(retryAttemptWarnThreshold > 0) @@ -1204,7 +1205,7 @@ object PekkoUtil extends HasLoggerName { private val logger = loggerFactory.getLogger(this.getClass) private implicit val directEC: ExecutionContext = DirectExecutionContext(logger) - private var consumer: Consumer[T] = Consumer.InitializationInProgress(initializationKillSwitch) + private var consumer: Consumer[T] = Consumer.InitializationInProgress private val recoveringQueue: RecoveringQueue[T] = new RecoveringQueue( maxBlocked = maxBlockedOffer, @@ -1215,7 +1216,7 @@ object PekkoUtil extends HasLoggerName { ) private val lock = new Mutex() private val timer: Timer = new Timer() - private var shuttingDown: Boolean = false + private val shuttingDown: AtomicBoolean = new AtomicBoolean(false) private var shuttingDownTimerCancelled: Boolean = false private val donePromise: Promise[Done] = Promise() private val firstSuccessfulConsumerInitializationPromise: Promise[Unit] = Promise() @@ -1229,7 +1230,7 @@ object PekkoUtil extends HasLoggerName { firstSuccessfulConsumerInitializationPromise.future override def offer(elem: T): Future[Done] = blockingSynchronized { - if (shuttingDown) { + if (shuttingDown.get()) { Future.failed( new IllegalStateException( "Cannot offer new elements to the queue, after shutdown is initiated" @@ -1243,7 +1244,7 @@ object PekkoUtil extends HasLoggerName { } override def shutdown(): Unit = blockingSynchronized { - if (shuttingDown || shuttingDownTimerCancelled) { + if (shuttingDown.get() || shuttingDownTimerCancelled) { logger.debug("Already shutting down, nothing to do") } else { shuttingDownTimerCancelled = true @@ -1267,23 +1268,17 @@ object PekkoUtil extends HasLoggerName { private def shutdownStepTwo(): Unit = blockingSynchronized { logger.info("Shutdown initiated") - shuttingDown = true + shuttingDown.set(true) recoveringQueue.shutdown() consumer match { case Consumer.Initialized(c) => logger.info("Consumer shutdown initiated") c.shutdown() - case Consumer.InitializationInProgress(killSwitch) => - killSwitch match { - case Some(kill) => - logger.info( - "Consumer initialization is in progress, invoking initialization kill switch..." - ) - kill() - case None => - logger.debug("Consumer initialization is in progress, delaying shutdown...") - } + case Consumer.InitializationInProgress => + logger.debug( + "Consumer initialization is in progress, shutdown signal will be propagated to consumer..." + ) case Consumer.WaitingForRetry => logger.info("Interrupting wait for initialization retry, shutdown complete") @@ -1293,8 +1288,12 @@ object PekkoUtil extends HasLoggerName { private def initializeConsumer(attempt: Int = 1): Unit = blockingSynchronized { logger.info("Initializing consumer...") - consumer = Consumer.InitializationInProgress(initializationKillSwitch) - consumerFactory(recoveringQueue.commit) + consumer = Consumer.InitializationInProgress + consumerFactory(recoveringQueue.commit)(() => shuttingDown.get()) + .flatMap { innerFuture => + firstSuccessfulConsumerInitializationPromise.trySuccess(()).discard + innerFuture + }(directEC) .onComplete(consumerInitialized(_, attempt))(directEC) } @@ -1311,14 +1310,13 @@ object PekkoUtil extends HasLoggerName { logger.error(s"Exception caught while recovering: ${t.getMessage}. Shutting down.", t) shutdown() } - if (shuttingDown) { + if (shuttingDown.get()) { logger.info( "Consumer initialized, but since shutdown already in progress, consumer shutdown initiated" ) queueConsumer.futureQueue.shutdown() queueConsumer.futureQueue.done.onComplete(consumerTerminated)(directEC) } else { - firstSuccessfulConsumerInitializationPromise.trySuccess(()).discard logger.info("Consumer initialized") consumer = Consumer.Initialized( new FutureQueuePullProxy( @@ -1334,7 +1332,7 @@ object PekkoUtil extends HasLoggerName { } case Failure(failure) => - if (shuttingDown) { + if (shuttingDown.get()) { logger.info( "Consumer initialization failed, but not retrying anymore since already shutting down", failure, @@ -1368,7 +1366,7 @@ object PekkoUtil extends HasLoggerName { case Failure(failure) => logger.info("Consumer terminated with a failure", failure) } - if (shuttingDown) { + if (shuttingDown.get()) { logger.info("Terminated (consumer terminated), shutdown complete") discard(donePromise.trySuccess(Done)) } else { @@ -1395,8 +1393,7 @@ object PekkoUtil extends HasLoggerName { } private object Consumer { - final case class InitializationInProgress(killSwitch: Option[() => Unit]) - extends Consumer[Nothing] + case object InitializationInProgress extends Consumer[Nothing] case object WaitingForRetry extends Consumer[Nothing] diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala new file mode 100644 index 0000000000..0ee6a0930c --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/ShardedSequentialProcessingQueue.scala @@ -0,0 +1,174 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.util + +import cats.data.{EitherT, Nested} +import cats.syntax.functor.* +import com.digitalasset.canton.concurrent.FutureSupervisor +import com.digitalasset.canton.config.ProcessingTimeout +import com.digitalasset.canton.discard.Implicits.* +import com.digitalasset.canton.lifecycle.{ + FlagCloseable, + FutureUnlessShutdown, + PromiseUnlessShutdown, +} +import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} +import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} +import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.Thereafter.syntax.ThereafterOps +import com.google.common.annotations.VisibleForTesting + +import scala.collection.concurrent.TrieMap +import scala.concurrent.ExecutionContext + +/** A processing queue that allows scheduling work associated with a particular identifier. + * + * It comes in two flavours: + * - [[GarbageCollectedShardedSequentialProcessingQueue]] when the number of identifiers is big + * and each identifier is usually short-lived. + * - [[NonGarbageCollectedShardedSequentialProcessingQueue]] when the number of identifiers is + * small and each identifier is potentially long-lived. + * + * @tparam Ident + * The type of the identifiers + */ +sealed trait ShardedSequentialProcessingQueue[Ident] { + def executeUS[A](id: Ident)(action: => FutureUnlessShutdown[A], taskName: String)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[A] + + def executeEUS[A, B](id: Ident)(action: => EitherT[FutureUnlessShutdown, A, B], taskName: String)( + implicit traceContext: TraceContext + ): EitherT[FutureUnlessShutdown, A, B] = EitherT(executeUS(id)(action.value, taskName)) + + @VisibleForTesting + def isQueueEmpty(id: Ident): Boolean + + @VisibleForTesting + def areQueuesEmpty: Boolean +} + +/** A processing queue that runs units of work for a particular identifier in a sequential manner, + * but allows parallel processing of work for different identifiers. If a unit of work fails or + * throws an exception, subsequent units of work for the same identifier are not executed. + * + * Use this queue when the number of identifiers is big and each identifier is usually short-lived. + * + * @tparam Ident + * The type of the identifiers + */ +class GarbageCollectedShardedSequentialProcessingQueue[Ident](implicit ec: ExecutionContext) + extends ShardedSequentialProcessingQueue[Ident] { + + @VisibleForTesting + val processingQueuePerId = new TrieMap[Ident, FutureUnlessShutdown[Unit]]() + + override def isQueueEmpty(id: Ident): Boolean = !processingQueuePerId.isDefinedAt(id) + override def areQueuesEmpty: Boolean = processingQueuePerId.isEmpty + + override def executeUS[A]( + id: Ident + )(action: => FutureUnlessShutdown[A], taskName: String)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[A] = { + val processingPromise: PromiseUnlessShutdown[Unit] = PromiseUnlessShutdown.unsupervised() + val processingFuture = processingPromise.futureUS + + val previousProcessingFuture: FutureUnlessShutdown[Unit] = processingQueuePerId + .put(id, processingFuture) + .getOrElse(FutureUnlessShutdown.unit) + + previousProcessingFuture.flatMap(_ => action).thereafter { result => + processingPromise.complete(Nested(result).void.value) + // cleanup the processing queue + processingQueuePerId + .updateWith(id) { + case Some(`processingFuture`) => + // if the "processing queue" still contains the same future that we put in, we can remove the entry from the map + None + case Some(other) => + // some other future was put into the map, retain it + Some(other) + case None => + // the entry was already removed, nothing to do + None + } + .discard + + } + } +} + +/** A processing queue that runs units of work for a particular identifier in a sequential manner, + * but allows parallel processing of work for different identifiers. For an identifier, the + * behavior of the queue when one task fails or throws an exception depends on the failureMode. + * + * If a unit of work fails or throws an exception, subsequent units of work for the same identifier + * are not executed. + * + * IMPORTANT NOTE: The underlying execution queues for the identifiers are not cleaned up. If you + * have many short-lived identifier, then [[GarbageCollectedShardedSequentialProcessingQueue]] + * should be preferred. + * + * Use this queue when the number of identifiers is small and each identifier is potentially + * long-lived. + * + * @param name + * For logging purposes + * @param logTaskTiming + * If true logs wait and run time for each of the tasks + * @param failureMode + * How the queue handles the execution of tasks after a previous task had failed + * @tparam Ident + * The type of the identifiers + */ +class NonGarbageCollectedShardedSequentialProcessingQueue[Ident: Pretty]( + private val name: String, + futureSupervisor: FutureSupervisor, + override val timeouts: ProcessingTimeout, + override val loggerFactory: NamedLoggerFactory, + private val logTaskTiming: Boolean, + failureMode: FailureMode, +) extends ShardedSequentialProcessingQueue[Ident] + with PrettyPrinting + with NamedLogging + with FlagCloseable { + + private val processingQueues = new TrieMap[Ident, SimpleExecutionQueue]() + + override def isQueueEmpty(id: Ident): Boolean = processingQueues.get(id).fold(true)(_.isEmpty) + override def areQueuesEmpty: Boolean = processingQueues.forall { case (_, queue) => + queue.isEmpty + } + + override def executeUS[A]( + id: Ident + )(action: => FutureUnlessShutdown[A], taskName: String)(implicit + traceContext: TraceContext + ): FutureUnlessShutdown[A] = { + + val queue = processingQueues.getOrElseUpdate( + id, + new SimpleExecutionQueue( + name = s"$name-$id", + futureSupervisor = futureSupervisor, + timeouts = timeouts, + loggerFactory = loggerFactory, + logTaskTiming = logTaskTiming, + failureMode = failureMode, + ), + ) + + queue.executeUS(action, taskName) + } + + override protected def pretty + : Pretty[NonGarbageCollectedShardedSequentialProcessingQueue[Ident]] = + prettyOfClass( + param("tasks", _.processingQueues) + ) + + override protected def onClosed(): Unit = + processingQueues.readOnlySnapshot().values.foreach(_.onClosed()) +} diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala index 6c4d60d427..43d8a637a1 100644 --- a/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/SimpleExecutionQueue.scala @@ -13,6 +13,11 @@ import com.digitalasset.canton.lifecycle.UnlessShutdown.AbortedDueToShutdown import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.canton.util.FailureMode.{ + ContinueAfterFailure, + CrashAfterFailure, + StopAfterFailure, +} import com.digitalasset.canton.util.ShowUtil.* import com.digitalasset.canton.util.SimpleExecutionQueue.TaskCell import com.digitalasset.canton.util.Thereafter.syntax.* @@ -27,18 +32,21 @@ import scala.util.{Failure, Success, Try} */ sealed trait FailureMode -/** Causes the queue to crash the entire process if a task is scheduled after a previously failed - * task. - */ -object CrashAfterFailure extends FailureMode +object FailureMode { -/** Causes the queue to not process any further tasks after a previously failed task. - */ -object StopAfterFailure extends FailureMode + /** Causes the queue to crash the entire process if a task is scheduled after a previously failed + * task. + */ + object CrashAfterFailure extends FailureMode -/** The queue will continue the execution of tasks even if previous tasks had failed. - */ -object ContinueAfterFailure extends FailureMode + /** Causes the queue to not process any further tasks after a previously failed task. + */ + object StopAfterFailure extends FailureMode + + /** The queue will continue the execution of tasks even if previous tasks had failed. + */ + object ContinueAfterFailure extends FailureMode +} /** Functions executed with this class will only run when all previous calls have completed * executing. This can be used when async code should not be run concurrently. @@ -86,7 +94,7 @@ class SimpleExecutionQueue( timeouts, loggerFactory, logTaskTiming, - if (crashOnFailure) CrashAfterFailure else StopAfterFailure, + failureMode = if (crashOnFailure) CrashAfterFailure else StopAfterFailure, ) protected val directExecutionContext: DirectExecutionContext = diff --git a/canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala b/canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala new file mode 100644 index 0000000000..beb681b233 --- /dev/null +++ b/canton/community/base/src/main/scala/com/digitalasset/canton/util/TracedPossiblyPrevalidated.scala @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.canton.util + +import com.digitalasset.canton.tracing.{TraceContext, Traced} + +/** Tag an object to have already passed some validation steps + * + * We use this specifically to optimistically validate signatures against older topology snapshots. + * If they pass validation (signature is valid and key is valid), we can then skip the more + * expensive validation steps and only validate the correct key usage. + */ +final case class TracedPossiblyPrevalidated[E](value: E, prevalidated: Boolean)(implicit + val traceContext: TraceContext +) { + def map[F](fn: E => F): TracedPossiblyPrevalidated[F] = + TracedPossiblyPrevalidated(fn(value), prevalidated) + + def tracedValue: Traced[E] = Traced(value)(traceContext) + + def withTraceContext[F](fn: TraceContext => E => F): F = fn(traceContext)(value) + +} + +object TracedPossiblyPrevalidated { + def notValidated[T](value: T)(implicit + traceContext: TraceContext + ): TracedPossiblyPrevalidated[T] = + TracedPossiblyPrevalidated(value, prevalidated = false) +} diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 new file mode 100644 index 0000000000..8ae5e4d649 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sha256 @@ -0,0 +1 @@ +4d9b1a8be5e6e28261f6306c7b4a356e4172cfd98512185b72c401901cbbbfe9 diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql new file mode 100644 index 0000000000..dd6b236801 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/dev/V5_3__tea_initial.sql @@ -0,0 +1,75 @@ +-- Traffic enforcement tables +create table par_traffic_enforcement_event +( + -- Increasing sequence number for each event + -- This is used to provide a total ordering of events + sequence_nb bigint generated always as identity, + -- Event source is a small integer that identifies the source of the event (e.g: 0 == LEDGER_API, 1 = TEA_API) + event_source smallint not null, + -- Event type is a small integer that identifies the type of event (e.g 0 == USAGE for standard traffic usage) + event_type smallint not null, + -- Event ID is a unique identifier per event_source, it is used for event deduplication + event_id varchar not null, + -- Account ID is the identifier of the account that the event applies to + account_id varchar not null, + -- Amount is the amount of traffic tied to the event, it can be positive or negative. + -- The actual semantics depend on the event type. + amount bigint not null, + -- Timestamp when the event was recorded + -- Critically this is NOT the record time of a transaction. It is generated by the database. + -- Note that it is possible for events to have timestamps reversed compared to their sequence number + timestamp bigint not null, + -- Events are unique per (event_source, event_id). + primary key (event_source, event_id) +); + +-- For fast querying by sequence number +create index idx_event_sequence on par_traffic_enforcement_event (sequence_nb); +-- For fast querying by account ID, event type and timestamp +create index account_id_timestamp_index on par_traffic_enforcement_event (account_id, event_type, timestamp); + +-- Balance table for each account +-- Contains one row per account, continuously updated with the total debits and credits for that account +-- as events are processed. +create table par_traffic_enforcement_balance +( + -- Account ID is the identifier of the account that the balance applies to + account_id varchar not null, + -- Event sequence number is the sequence number of the last event that was applied to the balance + event_sequence_nb bigint not null, + -- Matches the event_type of the event table. There's one row per account and event type. + event_type smallint not null, + -- Total debits is the total amount of traffic that has been debited from the account + total_debits bigint not null, + -- Total credits is the total amount of traffic that has been credited to the account + total_credits bigint not null, + -- Updated at is the timestamp of the last update to the balance. + -- It is at least as recent as the timestamp of the last event that was applied to the balance. + updated_at bigint not null, + -- One event type per account + primary key (account_id, event_type) +); + +-- Offset tables used by pekko projection +-- See https://pekko.apache.org/docs/pekko-projection/current/jdbc.html#schema +create table pekko_projection_offset_store +( + projection_name varchar not null, + projection_key varchar not null, + current_offset varchar not null, + manifest varchar not null, + mergeable boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); + +create index projection_name_index on pekko_projection_offset_store (projection_name); + +create table pekko_projection_management +( + projection_name varchar not null, + projection_key varchar not null, + paused boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); \ No newline at end of file diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 new file mode 100644 index 0000000000..82524ad01b --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sha256 @@ -0,0 +1 @@ +327fa628515fb0a00c639d2718cefaa37ad1566203ae157e6236a5914afbbc9e diff --git a/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql new file mode 100644 index 0000000000..7801bf7980 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/h2/stable/V5_2__dabft_partitioning.sql @@ -0,0 +1,8 @@ +drop table ord_availability_batch; +create table ord_availability_batch( + id varchar not null, + batch binary large object not null, + -- assigned at batch creation and used to calculate epoch expiration + epoch_number bigint not null, + primary key (epoch_number, id) +); diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 new file mode 100644 index 0000000000..18b4877df7 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sha256 @@ -0,0 +1 @@ +65699df26b7762f024355555db78f6d6e6f363fb2f2d9684fa5bcbbd09d589c4 diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql new file mode 100644 index 0000000000..d4c3323032 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/dev/V5_3__tea_initial.sql @@ -0,0 +1,75 @@ +-- Traffic enforcement tables +create table par_traffic_enforcement_event +( + -- Increasing sequence number for each event + -- This is used to provide a total ordering of events + sequence_nb bigint generated always as identity, + -- Event source is a small integer that identifies the source of the event (e.g: 0 == LEDGER_API, 1 = TEA_API) + event_source smallint not null, + -- Event type is a small integer that identifies the type of event (e.g 0 == USAGE for standard traffic usage) + event_type smallint not null, + -- Event ID is a unique identifier per event_source, it is used for event deduplication + event_id varchar collate "C" not null, + -- Account ID is the identifier of the account that the event applies to + account_id varchar collate "C" not null, + -- Amount is the amount of traffic tied to the event, it can be positive or negative. + -- The actual semantics depend on the event type. + amount bigint not null, + -- Timestamp when the event was recorded + -- Critically this is NOT the record time of a transaction. It is generated by the database. + -- Note that it is possible for events to have timestamps reversed compared to their sequence number + timestamp bigint not null, + -- Events are unique per (event_source, event_id). + primary key (event_source, event_id) +); + +-- For fast querying by sequence number +create index idx_event_sequence on par_traffic_enforcement_event (sequence_nb); +-- For fast querying by account ID, event type and timestamp +create index account_id_timestamp_index on par_traffic_enforcement_event (account_id, event_type, timestamp); + +-- Balance table for each account +-- Contains one row per account, continuously updated with the total debits and credits for that account +-- as events are processed. +create table par_traffic_enforcement_balance +( + -- Account ID is the identifier of the account that the balance applies to + account_id varchar collate "C" not null, + -- Event sequence number is the sequence number of the last event that was applied to the balance + event_sequence_nb bigint not null, + -- Matches the event_type of the event table. There's one row per account and event type. + event_type smallint not null, + -- Total debits is the total amount of traffic that has been debited from the account + total_debits bigint not null, + -- Total credits is the total amount of traffic that has been credited to the account + total_credits bigint not null, + -- Updated at is the timestamp of the last update to the balance. + -- It is at least as recent as the timestamp of the last event that was applied to the balance. + updated_at bigint not null, + -- One event type per account + primary key (account_id, event_type) +); + +-- Offset tables used by pekko projection +-- See https://pekko.apache.org/docs/pekko-projection/current/jdbc.html#schema +create table pekko_projection_offset_store +( + projection_name varchar collate "C" not null, + projection_key varchar collate "C" not null, + current_offset varchar collate "C" not null, + manifest varchar collate "C" not null, + mergeable boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); + +create index projection_name_index on pekko_projection_offset_store (projection_name); + +create table pekko_projection_management +( + projection_name varchar collate "C" not null, + projection_key varchar collate "C" not null, + paused boolean not null, + last_updated bigint not null, + primary key (projection_name, projection_key) +); \ No newline at end of file diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 new file mode 100644 index 0000000000..a4578f2695 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sha256 @@ -0,0 +1 @@ +12fb43a202d6a5927ba49d647dcfc73aff99a5807f3e5d0ed506f8be09f916be diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql new file mode 100644 index 0000000000..8b0b70b1f7 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_1__optimize_contract_key_lookup_index.sql @@ -0,0 +1,6 @@ +-- Copyright (c) 2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- Recreate the activate contract key index with internal_contract_id as an included column. +drop index lapi_events_activate_contract_key_idx; +create index lapi_events_activate_contract_key_idx on lapi_events_activate_contract using btree (create_key_hash, event_sequential_id) include (internal_contract_id) where create_key_hash is not null; diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 new file mode 100644 index 0000000000..d03ff16a35 --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sha256 @@ -0,0 +1 @@ +d4263917805c62bd820e184206f217fa074e727ffa7414a843a71adec87b1baa diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql new file mode 100644 index 0000000000..07835ba25b --- /dev/null +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/stable/V5_2__dabft_partitioning.sql @@ -0,0 +1,293 @@ +drop view debug.ord_epochs; +drop view debug.ord_availability_batch; +drop view debug.ord_pbft_messages_in_progress; +drop view debug.ord_pbft_messages_completed; +drop view debug.ord_metadata_output_blocks; +drop view debug.ord_metadata_output_epochs; +drop view debug.ord_leader_selection_state; + +alter table ord_epochs rename to ord_epochs_old; +alter table ord_availability_batch rename to ord_availability_batch_old; +alter table ord_pbft_messages_in_progress rename to ord_pbft_messages_in_progress_old; +alter table ord_pbft_messages_completed rename to ord_pbft_messages_completed_old; +alter table ord_metadata_output_blocks rename to ord_metadata_output_blocks_old; +alter table ord_metadata_output_epochs rename to ord_metadata_output_epochs_old; +alter table ord_leader_selection_state rename to ord_leader_selection_state_old; + +-- Stores metadata for epochs +-- Individual blocks/transactions exist in separate table +create table ord_epochs ( + -- strictly-increasing, contiguous epoch number + epoch_number bigint not null primary key, + -- first block sequence number (globally) of the epoch + start_block_number bigint not null, + -- number of total blocks in the epoch + epoch_length bigint not null, + -- Sequencing instant of the topology snapshot in force for the epoch + topology_ts bigint not null, + -- whether the epoch is in progress + in_progress bool not null +) partition by range (epoch_number); + +create table ord_availability_batch ( + id varchar collate "C" not null, + batch bytea not null, + -- assigned at batch creation and used to calculate epoch expiration + epoch_number bigint not null, + primary key (epoch_number, id) +) partition by range (epoch_number); + +-- messages stored during the progress of a block possibly across different pbft views +create table ord_pbft_messages_in_progress( + -- global sequence number of the ordered block + block_number bigint not null, + + -- epoch number of the block + epoch_number bigint not null, + + -- view number + view_number bigint not null, + + -- pbft message for the block + message bytea not null, + + -- pbft message discriminator (0 = pre-prepare, 1 = prepare, 2 = commit) + discriminator smallint not null, + + -- sender of the message + from_sequencer_id varchar collate "C" not null, + + -- for each block number, we only expect one message of each kind for the same sender and view number. + primary key (epoch_number, block_number, view_number, from_sequencer_id, discriminator) +) partition by range (epoch_number); + +create table ord_pbft_messages_completed( + -- global sequence number of the ordered block + block_number bigint not null, + + -- epoch number of the block + epoch_number bigint not null, + + -- pbft message for the block + message bytea not null, + + -- pbft message discriminator (0 = pre-prepare, 2 = commit) + discriminator smallint not null, + + -- sender of the message + from_sequencer_id varchar collate "C" not null, + + -- for each completed block number, we only expect one message of each kind for the same sender. + -- in the case of pre-prepare, we only expect one message for the whole block, but for simplicity + -- we won't differentiate that at the database level. + primary key (epoch_number, block_number, from_sequencer_id, discriminator) +) partition by range (epoch_number); + +-- Stores metadata for blocks that have been assigned timestamps in the output module +create table ord_metadata_output_blocks ( + epoch_number bigint not null, + block_number bigint not null, + bft_ts bigint not null, + primary key (epoch_number, block_number) +) partition by range (epoch_number); + +create index idx_ord_metadata_output_blocks_bft_ts on ord_metadata_output_blocks(bft_ts); +create index idx_ord_metadata_output_blocks_block_number on ord_metadata_output_blocks(block_number); + +-- Stores output metadata for epochs +create table ord_metadata_output_epochs ( + epoch_number bigint not null primary key, + could_alter_ordering_topology bool not null +) partition by range (epoch_number); + +create table ord_leader_selection_state ( + epoch_number bigint not null primary key, + state bytea not null +) partition by range (epoch_number); + +create function create_partitions_for_epoch_range( + parent_table regclass, + min_val bigint, + max_val bigint +) returns void as $$ +declare + partition_size bigint := ${initialBftOrdererTablesPartitionSize}; + current_val bigint; + from_val bigint; + next_val bigint; + final_val bigint; + partition_index bigint; + partition_name text; +begin + if partition_size <= 0 then + raise exception 'partition_size must be positive'; + end if; + + current_val := floor(min_val::numeric / partition_size)::bigint * partition_size; + + -- creates one extra partition after the partition containing max_val + final_val := max_val + partition_size; + + while current_val <= final_val loop + next_val := current_val + partition_size; + partition_index := current_val / partition_size; + partition_name := format( + '%s_p%s', + replace(parent_table::text, '.', '_'), + partition_index + ); + + -- the first partition will start at -1 + if current_val = 0 then + from_val := -1; + else + from_val := current_val; + end if; + + execute format( + 'create table if not exists %I partition of %s for values from (%L) to (%L)', + partition_name, + parent_table::text, + from_val, + next_val + ); + + current_val := next_val; + end loop; +end; +$$ language plpgsql; + +do $$ +declare + min_epoch bigint; + max_epoch bigint; +begin + -- Use a shared epoch range across ord_epochs_old and ord_availability_batch_old. + -- ord_availability_batch_old can contain epochs ahead of ord_epochs_old, + -- while other transient tables (ord_pbft_messages_in_progress) may be empty during the migration. + select min(epoch_number), max(epoch_number) + into min_epoch, max_epoch + from ( + select epoch_number from ord_epochs_old + union all + select epoch_number from ord_availability_batch_old + ) all_epochs; + + if min_epoch is null or max_epoch is null then + min_epoch := 0; + max_epoch := 0; + end if; + + perform create_partitions_for_epoch_range('ord_epochs'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_availability_batch'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_pbft_messages_in_progress'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_pbft_messages_completed'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_metadata_output_blocks'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_metadata_output_epochs'::regclass, min_epoch, max_epoch); + perform create_partitions_for_epoch_range('ord_leader_selection_state'::regclass, min_epoch, max_epoch); +end $$; + +insert into ord_epochs (epoch_number, start_block_number, epoch_length, topology_ts, in_progress) +select epoch_number, start_block_number, epoch_length, topology_ts, in_progress +from ord_epochs_old; + +insert into ord_availability_batch (id, batch, epoch_number) +select id, batch, epoch_number +from ord_availability_batch_old; + +insert into ord_pbft_messages_in_progress (block_number, epoch_number, view_number, message, discriminator, from_sequencer_id) +select block_number, epoch_number, view_number, message, discriminator, from_sequencer_id +from ord_pbft_messages_in_progress_old; + +insert into ord_pbft_messages_completed (block_number, epoch_number, message, discriminator, from_sequencer_id) +select block_number, epoch_number, message, discriminator, from_sequencer_id +from ord_pbft_messages_completed_old; + +insert into ord_metadata_output_blocks (epoch_number, block_number, bft_ts) +select epoch_number, block_number, bft_ts +from ord_metadata_output_blocks_old; + +insert into ord_metadata_output_epochs (epoch_number, could_alter_ordering_topology) +select epoch_number, could_alter_ordering_topology +from ord_metadata_output_epochs_old; + +insert into ord_leader_selection_state (epoch_number, state) +select epoch_number, state +from ord_leader_selection_state_old; + +drop table ord_epochs_old; +drop table ord_availability_batch_old; +drop table ord_pbft_messages_in_progress_old; +drop table ord_pbft_messages_completed_old; +drop table ord_metadata_output_blocks_old; +drop table ord_metadata_output_epochs_old; +drop table ord_leader_selection_state_old; + +create table ord_partition_size_history ( + epoch_number bigint not null primary key, + partition_number bigint not null, + partition_size integer not null +); +insert into ord_partition_size_history(epoch_number, partition_number, partition_size) + values (0, 0, ${initialBftOrdererTablesPartitionSize}); + +create or replace view debug.ord_epochs as +select + epoch_number, + start_block_number, + epoch_length, + debug.canton_timestamp(topology_ts) as topology_ts, + in_progress +from ord_epochs; + +create or replace view debug.ord_availability_batch as +select + id, + batch, + epoch_number +from ord_availability_batch; + +create or replace view debug.ord_pbft_messages_in_progress as +select + block_number, + epoch_number, + view_number, + message, + discriminator, + from_sequencer_id +from ord_pbft_messages_in_progress; + +create or replace view debug.ord_pbft_messages_completed as +select + block_number, + epoch_number, + message, + discriminator, + from_sequencer_id +from ord_pbft_messages_completed; + +create or replace view debug.ord_metadata_output_blocks as +select + epoch_number, + block_number, + debug.canton_timestamp(bft_ts) as bft_ts +from ord_metadata_output_blocks; + +create or replace view debug.ord_metadata_output_epochs as +select + epoch_number, + could_alter_ordering_topology +from ord_metadata_output_epochs; + +create or replace view debug.ord_leader_selection_state as +select + epoch_number, + state +from ord_leader_selection_state; + +create or replace view debug.ord_partition_size_history as +select + epoch_number, + partition_number, + partition_size +from ord_partition_size_history; diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 index 005d982cdb..9b76b97111 100644 --- a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sha256 @@ -1 +1 @@ -ba9abfee60cf3b9023278798a98247376a623edc14fc6873c01e0c9cdfaa3491 +33e30818cb98745e054432c6e6cd015c7160cd1918bcf136cd23a338f32c5d7b diff --git a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql index 6b371faf21..2ac380c719 100644 --- a/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql +++ b/canton/community/common/src/main/resources/db/migration/canton/postgres/table_settings/R___default_auto_vacuum_analyze_table_settings.sql @@ -55,65 +55,3 @@ alter table seq_in_flight_aggregation autovacuum_freeze_max_age = 600000000, autovacuum_freeze_table_age = 600000000 ); - --- ====== BFT ordering autovacuum settings ===== -alter table ord_epochs - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_availability_batch - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_pbft_messages_in_progress - set ( - autovacuum_vacuum_scale_factor = 0.0, - autovacuum_vacuum_threshold = 10000, - autovacuum_vacuum_cost_limit = 2000, - autovacuum_vacuum_cost_delay = 5, - autovacuum_vacuum_insert_scale_factor = 0.0, - autovacuum_vacuum_insert_threshold = 100000, - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_pbft_messages_completed - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_metadata_output_blocks - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_metadata_output_epochs - set ( - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); - -alter table ord_leader_selection_state - set ( - autovacuum_vacuum_scale_factor = 0.0, - autovacuum_vacuum_threshold = 10000, - autovacuum_vacuum_cost_limit = 2000, - autovacuum_vacuum_cost_delay = 5, - autovacuum_vacuum_insert_scale_factor = 0.0, - autovacuum_vacuum_insert_threshold = 100000, - autovacuum_freeze_min_age = 1000000, - autovacuum_freeze_max_age = 600000000, - autovacuum_freeze_table_age = 600000000 - ); diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala index 3cf9311836..5742afb50a 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/environment/CantonNodeBootstrap.scala @@ -63,7 +63,7 @@ import com.digitalasset.canton.lifecycle.{ } import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.metrics.ActiveRequestsMetrics.GrpcServerMetricsX -import com.digitalasset.canton.metrics.{DbStorageMetrics, DeclarativeApiMetrics} +import com.digitalasset.canton.metrics.{CryptoMetrics, DbStorageMetrics, DeclarativeApiMetrics} import com.digitalasset.canton.networking.grpc.{ CantonGrpcUtil, CantonMutableHandlerRegistry, @@ -181,6 +181,7 @@ trait BaseMetrics { new CacheMetrics("topology", openTelemetryMetricsFactory) def healthMetrics: HealthMetrics def storageMetrics: DbStorageMetrics + def cryptoMetrics: CryptoMetrics val declarativeApiMetrics: DeclarativeApiMetrics } @@ -492,6 +493,7 @@ abstract class CantonNodeBootstrapImpl[ ReleaseProtocolVersion.latest, arguments.futureSupervisor, arguments.clock, + arguments.metrics.cryptoMetrics, executionContext, bootstrapStageCallback.timeouts, arguments.config.parameters.batching, @@ -1268,6 +1270,7 @@ abstract class CantonNodeBootstrapImpl[ mapping, serial = None, keys, + namespacesToSignFor = Seq.empty, protocolVersion, expectFullAuthorization = true, waitToBecomeEffective = None, diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala index 644de6ea87..efa8d4a987 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerReadService.scala @@ -724,13 +724,15 @@ class GrpcTopologyManagerReadService( override def exportTopologySnapshot( request: ExportTopologySnapshotRequest, responseObserver: StreamObserver[ExportTopologySnapshotResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient[ExportTopologySnapshotResponse]( (out: OutputStream) => getTopologySnapshot(request, out), responseObserver, byteString => ExportTopologySnapshotResponse(byteString), processingTimeout.unbounded.duration, ) + } private def getTopologySnapshot( request: ExportTopologySnapshotRequest, @@ -759,13 +761,15 @@ class GrpcTopologyManagerReadService( override def exportTopologySnapshotV2( request: ExportTopologySnapshotV2Request, responseObserver: StreamObserver[ExportTopologySnapshotV2Response], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient[ExportTopologySnapshotV2Response]( (out: OutputStream) => getTopologySnapshotV2(request, out), responseObserver, byteString => ExportTopologySnapshotV2Response(byteString), processingTimeout.unbounded.duration, ) + } private def getTopologySnapshotV2( request: ExportTopologySnapshotV2Request, @@ -844,13 +848,15 @@ class GrpcTopologyManagerReadService( override def genesisState( request: GenesisStateRequest, responseObserver: StreamObserver[GenesisStateResponse], - ): Unit = + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext GrpcStreamingUtils.streamToClient( (out: OutputStream) => getGenesisState(request.synchronizerStore, request.timestamp, out), responseObserver, byteString => GenesisStateResponse(byteString), processingTimeout.unbounded.duration, ) + } private def getGenesisState( filterSynchronizerStore: Option[StoreId], @@ -867,12 +873,15 @@ class GrpcTopologyManagerReadService( override def genesisStateV2( request: GenesisStateV2Request, responseObserver: StreamObserver[GenesisStateV2Response], - ): Unit = GrpcStreamingUtils.streamToClient( - (out: OutputStream) => getGenesisStateV2(request.synchronizerStore, request.timestamp, out), - responseObserver, - byteString => GenesisStateV2Response(byteString), - processingTimeout.unbounded.duration, - ) + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + GrpcStreamingUtils.streamToClient( + (out: OutputStream) => getGenesisStateV2(request.synchronizerStore, request.timestamp, out), + responseObserver, + byteString => GenesisStateV2Response(byteString), + processingTimeout.unbounded.duration, + ) + } private def getGenesisStateV2( filterSynchronizerStore: Option[StoreId], @@ -949,13 +958,16 @@ class GrpcTopologyManagerReadService( override def sequencerLsuState( request: SequencerLsuStateRequest, responseObserver: StreamObserver[SequencerLsuStateResponse], - ): Unit = GrpcStreamingUtils.streamToClient( - (out: OutputStream) => - getLogicalUpgradeState(request.synchronizerStore, request.timestamp, out), - responseObserver, - byteString => SequencerLsuStateResponse(byteString), - processingTimeout.unbounded.duration, - ) + ): Unit = { + implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext + GrpcStreamingUtils.streamToClient( + (out: OutputStream) => + getLogicalUpgradeState(request.synchronizerStore, request.timestamp, out), + responseObserver, + byteString => SequencerLsuStateResponse(byteString), + processingTimeout.unbounded.duration, + ) + } private def getLogicalUpgradeState( synchronizerStore: Option[StoreId], diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala index e76da95d8b..d1e94e1007 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/topology/admin/grpc/GrpcTopologyManagerWriteService.scala @@ -148,6 +148,7 @@ class GrpcTopologyManagerWriteService( validatedMapping, serial, signingKeys, + namespacesToSignFor = Seq.empty, manager.managerVersion.serialization, expectFullAuthorization = mustFullyAuthorize, forceChanges = forceChanges, @@ -184,7 +185,7 @@ class GrpcTopologyManagerWriteService( extendedTransactions <- signedTxs.parTraverse(tx => targetManager - .extendSignature(tx, signingKeys, forceFlags) + .extendSignature(tx, signingKeys, namespacesToSignFor = Seq.empty, forceFlags) .leftWiden[RpcError] ) } yield extendedTransactions diff --git a/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala b/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala index 0b72438085..1a45d4ae41 100644 --- a/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala +++ b/canton/community/common/src/main/scala/com/digitalasset/canton/util/GrpcStreamingUtils.scala @@ -7,8 +7,10 @@ import better.files.* import better.files.File.newTemporaryFile import com.digitalasset.canton.ProtoDeserializationError import com.digitalasset.canton.config.DefaultProcessingTimeouts +import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.grpc.ByteStringStreamObserverWithContext import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.lifecycle.FutureUnlessShutdownImpl.thereafterFutureUnlessShutdown import com.digitalasset.canton.lifecycle.UnlessShutdown.{AbortedDueToShutdown, Outcome} import com.digitalasset.canton.logging.ErrorLoggingContext import com.digitalasset.canton.networking.grpc.CantonGrpcUtil.GrpcErrors @@ -21,28 +23,22 @@ import com.digitalasset.canton.version.{ VersioningCompanion, } import com.google.protobuf.ByteString -import io.grpc.Context -import io.grpc.stub.StreamObserver +import io.grpc.stub.{ServerCallStreamObserver, StreamObserver} +import io.grpc.{Context, Status} import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.{Source, Source as PekkoSource} -import java.io.{ - BufferedInputStream, - ByteArrayInputStream, - ByteArrayOutputStream, - InputStream, - OutputStream, - PipedInputStream, - PipedOutputStream, -} +import java.io.{InputStream, OutputStream, PipedInputStream, PipedOutputStream} import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import scala.annotation.tailrec -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{Duration, DurationInt} import scala.concurrent.{Await, ExecutionContext, Future, Promise, blocking} +import scala.util.control.NonFatal import scala.util.{Failure, Success, Try} object GrpcStreamingUtils { + private[util] final val defaultChunkSize: Int = 1024 * 1024 * 2 // 2MB - This is half of the default max message size of gRPC @@ -276,22 +272,54 @@ object GrpcStreamingUtils { fromByteString: FromByteString[T], processingTimeout: Duration = DefaultProcessingTimeouts.unbounded.duration, chunkSizeO: Option[Int] = None, - )(implicit ec: ExecutionContext): Unit = { - val context = io.grpc.Context - .current() - .withCancellation() + )(implicit ec: ExecutionContext, loggingContext: ErrorLoggingContext): Unit = { + val context = io.grpc.Context.current().withCancellation() + val chunkSize = chunkSizeO.getOrElse(defaultChunkSize) + val pipedInput = new PipedInputStream(chunkSize) + // Introduced so that clients do not have to enclose calls in blocking + val blockingPipedOutput = new PipedOutputStream(pipedInput) { + override def write(b: Int): Unit = blocking(super.write(b)) + + override def write(b: Array[Byte], off: Int, len: Int): Unit = blocking { + super.write(b, off, len) + } + + override def write(b: Array[Byte]): Unit = blocking(super.write(b)) + } + + def closeQuietly(c: java.io.Closeable): Unit = Try(c.close()).discard - val outputStream = new ByteArrayOutputStream() context.run { () => - val processingResult = responseF(outputStream).map { _ => - val chunkSize = chunkSizeO.getOrElse(defaultChunkSize) - val inputStream = new ByteArrayInputStream(outputStream.toByteArray) + // Close both piped streams on context cancellation so the producer unblocks promptly + context.addListener( + (_: Context) => { + closeQuietly(pipedInput) + closeQuietly(blockingPipedOutput) + }, + (command: Runnable) => command.run(), + ) + + val consumerF = streamResponseChunks(context, responseObserver)( - new BufferedInputStream(inputStream), + pipedInput, chunkSize, fromByteString, ) + + val producerF = responseF(blockingPipedOutput).transform { result => + val closeResult = Try(blockingPipedOutput.close()) + result match { + case Success(_) => closeResult + case Failure(ex) => + closeResult.forFailed(ex.addSuppressed(_)) + result + } } + + val processingResult = producerF + .flatMap(_ => consumerF) + .thereafter(_ => closeQuietly(pipedInput)) + finishStream(context, responseObserver)(processingResult, processingTimeout) } } @@ -302,7 +330,7 @@ object GrpcStreamingUtils { fromByteString: FromByteString[T], processingTimeout: Duration = DefaultProcessingTimeouts.unbounded.duration, chunkSizeO: Option[Int] = None, - )(implicit ec: ExecutionContext): Unit = { + )(implicit ec: ExecutionContext, loggingContext: ErrorLoggingContext): Unit = { val file = newTemporaryFile() val context = io.grpc.Context @@ -310,7 +338,7 @@ object GrpcStreamingUtils { .withCancellation() context.run { () => - val processingResult = responseF(file).map { _ => + val processingResult = responseF(file).flatMap { _ => val chunkSize = chunkSizeO.getOrElse(defaultChunkSize) streamResponseChunks(context, responseObserver)( file.newInputStream.buffered(chunkSize), @@ -378,42 +406,123 @@ object GrpcStreamingUtils { read(Nil) } - private def streamResponseChunks[T]( + private[util] def streamResponseChunks[T]( context: Context.CancellableContext, responseObserver: StreamObserver[T], )( inputStream: InputStream, chunkSize: Int, fromByteString: FromByteString[T], - ): Unit = - inputStream.autoClosed { s => - Iterator - .continually(s.readNBytes(chunkSize)) + )(implicit + executionContext: ExecutionContext, + loggingContext: ErrorLoggingContext, + ): Future[Unit] = + withServerCallStreamObserverF(responseObserver) { scso => + val iter = Iterator + .continually(inputStream.readNBytes(chunkSize)) // Before pushing new chunks to the stream, keep checking that the context has not been cancelled // This avoids the server reading the entire dump file for nothing if the client has already cancelled .takeWhile(_.nonEmpty && !context.isCancelled) - .foreach { byteArray => - val chunk: ByteString = ByteString.copyFrom(byteArray) - responseObserver.onNext(fromByteString.toT(chunk)) - } + + val isWorkerRunning = new AtomicBoolean(false) + val allBytesWrittenPromise = Promise[Unit]() + + def runWorkerInBackground(): Unit = FutureUtil.doNotAwait( + Future { + // Returns true if the stream was exhausted (EOF reached), false if the loop stopped + // because the observer is no longer ready + Try { + @tailrec + def sendChunks(): Boolean = + if (!scso.isReady) { + false + } else if (iter.hasNext) { + val byteArray = iter.next() + val chunk: ByteString = ByteString.copyFrom(byteArray) + responseObserver.onNext(fromByteString.toT(chunk)) + sendChunks() + } else { + true + } + sendChunks() + } match { + case Failure(ex) => + isWorkerRunning.set(false) + allBytesWrittenPromise.tryFailure(ex).discard + case Success(exhausted) => + if (exhausted) { + allBytesWrittenPromise.trySuccess(()).discard + } + isWorkerRunning.set(false) + if (!exhausted) { + if (context.isCancelled) { + allBytesWrittenPromise.trySuccess(()).discard + } else if (scso.isReady) { + // this check is important in case the `onReadyHandler` gets triggered before setting `isWorkerRunning` to `false` above. Not triggering here would mean that the stream will stall. + triggerWorker() + } else { + // Fix for misbehaving observer - that never triggers onReady/onCancel + DelayUtil + .delay(100.millis) + .foreach(_ => triggerWorker().discard) + } + } + } + }, + "Cannot start streaming response to client", + ) + + def triggerWorker(): Future[Unit] = Future { + // Do not start a new worker once streaming is finished or the client has gone away, + // to avoid spinning up workers (and rescheduling them) for an already-completed stream. + if (context.isCancelled) { + allBytesWrittenPromise.trySuccess(()).discard + } else if ( + !allBytesWrittenPromise.isCompleted && isWorkerRunning + .compareAndSet(false, true) + ) + runWorkerInBackground() + } + + scso.setOnReadyHandler(() => triggerWorker().discard) + scso.setOnCancelHandler { () => + Try(inputStream.close()).discard + allBytesWrittenPromise.trySuccess(()).discard + } + + triggerWorker().discard + + allBytesWrittenPromise.future.thereafter(_ => Try(inputStream.close()).discard) } private def finishStream[T]( context: Context.CancellableContext, responseObserver: StreamObserver[T], - )(f: Future[Unit], timeout: Duration): Unit = - Try(Await.result(f, timeout)) match { - case Failure(exception) => + )(f: Future[Unit], timeout: Duration): Unit = { + def cancelContext(): Unit = { + context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) + () + } + + try { + Await.result(f, timeout) + if (!context.isCancelled) responseObserver.onCompleted() + else cancelContext() + } catch { + case _: InterruptedException => + // The serving thread was interrupted (e.g. node shutdown or call cancellation) while blocked + // in `Await.result`. Cancel the context to terminate the gRPC call exactly once (CANCELLED), + // and only restore the thread's interrupt status. We must NOT rethrow: letting the + // InterruptedException escape into the gRPC request handler makes it close the call a second + // time, writing to the already-terminated HTTP/2 stream, which Netty logs as + // "Stream closed before write could take place". + cancelContext() + Thread.currentThread().interrupt() + case NonFatal(exception) => responseObserver.onError(exception) - context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) - () - case Success(_) => - if (!context.isCancelled) responseObserver.onCompleted() - else { - context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) - () - } + cancelContext() } + } def futureUnlessShutdownToStreamObserver[A]( fus: FutureUnlessShutdown[A], @@ -426,6 +535,58 @@ object GrpcStreamingUtils { case Success(AbortedDueToShutdown) => observer.onError(GrpcErrors.AbortedDueToShutdown.Error().asGrpcError) } + + private def withServerCallStreamObserverG[R, A]( + observer: StreamObserver[R] + )(ifNotSupported: => A)( + handler: ServerCallStreamObserver[R] => A + )(implicit errorLoggingContext: ErrorLoggingContext): A = + observer match { + case serverCallStreamObserver: ServerCallStreamObserver[R] => + handler(serverCallStreamObserver) + case other => + val statusException = + Status.INTERNAL + .withDescription(s"Unknown stream observer request") + .asException() + errorLoggingContext.warn( + s"${statusException.getMessage} StreamObserver:(${other.getClass})", + statusException, + ) + observer.onError(statusException) + ifNotSupported + } + + /** Ensure the observer is a ServerCallStreamObserver, running `handler` if so. Otherwise reports + * an INTERNAL error to the observer. See `withServerCallStreamObserverG`. + * + * @param observer + * underlying observer + * @param handler + * handler requiring a ServerCallStreamObserver + */ + def withServerCallStreamObserver[R]( + observer: StreamObserver[R] + )(handler: ServerCallStreamObserver[R] => Unit)(implicit + errorLoggingContext: ErrorLoggingContext + ): Unit = + withServerCallStreamObserverG(observer)(())(handler) + + /** Ensure the observer is a ServerCallStreamObserver, running `handler` if so. Otherwise reports + * an INTERNAL error to the observer and returns a completed future. See + * `withServerCallStreamObserverG`. + * + * @param observer + * underlying observer + * @param handler + * handler requiring a ServerCallStreamObserver + */ + def withServerCallStreamObserverF[R]( + observer: StreamObserver[R] + )(handler: ServerCallStreamObserver[R] => Future[Unit])(implicit + errorLoggingContext: ErrorLoggingContext + ): Future[Unit] = + withServerCallStreamObserverG(observer)(Future.unit)(handler) } // Define a type class for converting ByteString to the generic type T diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala index fc7687c806..9b07fb6e33 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/kms/KmsTest.scala @@ -38,6 +38,7 @@ import com.digitalasset.canton.crypto.{ SigningPublicKey, } import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.util.Thereafter.syntax.* import com.digitalasset.canton.util.{ByteString190, ByteString256, ByteString4096} @@ -94,6 +95,7 @@ trait KmsTest extends BaseTest with BeforeAndAfterAll { SessionEncryptionKeyCacheConfig(), CachingConfigs.defaultPublicKeyConversionCache, CryptoSchemes.tryFromConfig(config), + CommonMockMetrics.cryptoMetrics, loggerFactory, ) .valueOrFail("create crypto with JCE provider") diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala index ec5311cce1..b0ea5359ff 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/jce/JceCryptoTest.scala @@ -19,6 +19,7 @@ import com.digitalasset.canton.crypto.CryptoKeyFormat.Raw import com.digitalasset.canton.crypto.CryptoTestHelper.TestMessage import com.digitalasset.canton.crypto.SigningKeySpec.EcSecp256k1 import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.tracing.NoReportingTracerProvider @@ -61,6 +62,7 @@ class JceCryptoTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executionContext, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala index a391d86633..afa455d0b4 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/provider/kms/KmsCryptoTest.scala @@ -21,6 +21,7 @@ import com.digitalasset.canton.config.{ import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.kms.Kms import com.digitalasset.canton.lifecycle.FutureUnlessShutdown +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage import com.digitalasset.canton.tracing.NoReportingTracerProvider @@ -87,6 +88,7 @@ trait KmsCryptoTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executorService, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala index ee1335ca74..dc56ab8381 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoTest.scala @@ -28,6 +28,7 @@ import com.digitalasset.canton.crypto.{ TestHash, } import com.digitalasset.canton.lifecycle.* +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage @@ -139,6 +140,7 @@ trait SyncCryptoTest testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executorService, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala index 47850f6612..aaf6c10867 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithLongTermKeysTest.scala @@ -39,7 +39,7 @@ class SyncCryptoWithLongTermKeysTest TestingTopology() .withSimpleParticipants(participant1) .withCryptoConfig( - cryptoConfigWithSessionSigningKeysConfig(SessionSigningKeysConfig.default) + cryptoConfigWithSessionSigningKeysConfig(SessionSigningKeysConfig.enabled) ) .build(crypto, loggerFactory) diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala index 9f90314c18..a78b14fa16 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/sync/SyncCryptoWithSessionKeysTest.scala @@ -36,7 +36,7 @@ class SyncCryptoWithSessionKeysTest with ProtocolVersionChecksAnyWordSpec { override protected lazy val sessionSigningKeysConfig: SessionSigningKeysConfig = - if (testedProtocolVersion >= ProtocolVersion.v35) SessionSigningKeysConfig.default + if (testedProtocolVersion >= ProtocolVersion.v35) SessionSigningKeysConfig.enabled else SessionSigningKeysConfig.disabled private var p1PV34: SynchronizerCryptoClient = _ @@ -601,7 +601,7 @@ class SyncCryptoWithSessionKeysTest val signature = loggerFactory.assertLoggedWarningsAndErrorsSeq( { - p1PV34 = createTestingTopologyWith(SessionSigningKeysConfig.default) + p1PV34 = createTestingTopologyWith(SessionSigningKeysConfig.enabled) .forOwnerAndSynchronizer(participant1) p1PV34.syncCryptoSigner.isInstanceOf[SyncCryptoSignerWithLongTermKeys] shouldBe true p1PV34.syncCryptoSigner diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala index ed5ad45a4b..0a1b1df707 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/crypto/validations/SyncSchemeValidationsTest.scala @@ -16,6 +16,7 @@ import com.digitalasset.canton.config.{ } import com.digitalasset.canton.crypto.* import com.digitalasset.canton.crypto.store.CryptoPrivateStoreExtended +import com.digitalasset.canton.metrics.CommonMockMetrics import com.digitalasset.canton.protocol.StaticSynchronizerParameters import com.digitalasset.canton.replica.ReplicaManager import com.digitalasset.canton.resource.MemoryStorage @@ -48,6 +49,7 @@ class SyncSchemeValidationsTest extends AnyWordSpec with BaseTest with HasExecut testedReleaseProtocolVersion, futureSupervisor, wallClock, + CommonMockMetrics.cryptoMetrics, executorService, timeouts, BatchingConfig(), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala index 5def0ffae6..462e15a3ac 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/SequencerClientTest.scala @@ -1822,7 +1822,7 @@ final class SequencerClientTest override protected def loggerFactory: NamedLoggerFactory = SequencerClientTest.this.loggerFactory - override def physicalSynchronizerIdO: Option[PhysicalSynchronizerId] = ??? + override def physicalSynchronizerIdO: Option[PhysicalSynchronizerId] = None override def staticSynchronizerParametersO: Option[StaticSynchronizerParameters] = ??? @@ -1884,6 +1884,8 @@ final class SequencerClientTest )(implicit traceContext: TraceContext ): Either[SequencerConnectionPoolError.ThresholdUnreachableError, Unit] = Either.unit + + override val metricsContext: MetricsContext = MetricsContext.Empty } private object MockPool { diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala index 7e136a22ea..0f1ac98fee 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/TestSequencerClientSend.scala @@ -7,6 +7,7 @@ import cats.data.{EitherT, Nested} import com.daml.metrics.api.MetricsContext import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.protocol.messages.DefaultOpenEnvelope +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.TestSequencerClientSend.Request import com.digitalasset.canton.sequencing.protocol.{ @@ -43,6 +44,7 @@ class TestSequencerClientSend(override protected[canton] val clock: Clock)(impli messageId: MessageId, aggregationRule: Option[AggregationRule], callback: SendCallback, + trafficCostValidator: TrafficCostValidator, amplify: Boolean, useConfirmationResponseAmplificationParameters: Boolean, )(implicit diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala index f0f29ae5a7..3e90226379 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/ConnectionPoolTestHelpers.scala @@ -222,6 +222,7 @@ trait ConnectionPoolTestHelpers { poolDelays: SequencerConnectionPoolDelays = SequencerConnectionPoolDelays.default, blockValidation: Int => Boolean = _ => false, metrics: SequencerConnectionPoolMetrics = CommonMockMetrics.sequencerClient.connectionPool, + metricsContext: MetricsContext = MetricsContext.Empty, namePrefix: String = "test", )( f: ( @@ -251,6 +252,7 @@ trait ConnectionPoolTestHelpers { testCrypto.crypto, Some(seedForRandomness), metrics = metrics, + metricsContext = metricsContext, futureSupervisor, testTimeouts, loggerFactory, @@ -286,7 +288,7 @@ trait ConnectionPoolTestHelpers { sequencerSubscriptionFactory = new TestSequencerSubscriptionFactory(timeouts, loggerFactory), subscriptionHandlerFactory = TestSubscriptionHandlerFactory, metrics = CommonMockMetrics.sequencerClient.connectionPool, - metricsContext = MetricsContext.Empty, + metricsContext = connectionPool.metricsContext, timeouts = timeouts, loggerFactory = loggerFactory, ) @@ -450,6 +452,7 @@ protected object ConnectionPoolTestHelpers { crypto: Crypto, seedForRandomnessO: Option[Long], metrics: SequencerConnectionPoolMetrics, + metricsContext: MetricsContext, futureSupervisor: FutureSupervisor, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, @@ -462,6 +465,7 @@ protected object ConnectionPoolTestHelpers { responsesForConnection, validationBlocker, metrics, + metricsContext, futureSupervisor, timeouts, loggerFactory, @@ -489,7 +493,7 @@ protected object ConnectionPoolTestHelpers { crypto, seedForRandomnessO, metrics, - MetricsContext.Empty, + metricsContext, futureSupervisor, timeouts, loggerFactory, @@ -514,6 +518,7 @@ protected object ConnectionPoolTestHelpers { responsesForConnection: PartialFunction[Int, TestResponses], validationBlocker: TestValidationBlocker, metrics: SequencerConnectionPoolMetrics, + metricsContext: MetricsContext, futureSupervisor: FutureSupervisor, timeouts: ProcessingTimeout, loggerFactory: NamedLoggerFactory, @@ -559,7 +564,7 @@ protected object ConnectionPoolTestHelpers { ClientChannelParams.ForTesting, stubFactory = stubFactory, metrics = metrics, - metricsContext = MetricsContext.Empty, + metricsContext = metricsContext, futureSupervisor = futureSupervisor, timeouts = timeouts, loggerFactory = loggerFactory.append("connection", config.name), diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala index 257bda3ecf..999e4e85be 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/sequencing/client/pool/SequencerConnectionPoolImplTest.scala @@ -749,6 +749,9 @@ class SequencerConnectionPoolImplTest } } + // Closing the pool closes all the metrics + poolMetrics.connectionHealthMetrics shouldBe empty + withConnectionPool( nbConnections = PositiveInt.three, trustThreshold = PositiveInt.two, @@ -756,15 +759,6 @@ class SequencerConnectionPoolImplTest metrics = poolMetrics, // reuse the same metrics namePrefix = "second-config", ) { (pool, _, _, _) => - // when creating a new pool for the same synchronizer, the same metrics are reused and - // the connections from the first connect are still in there. - // this means, disconnecting from a synchronizer keeps the metric in fatal state - poolMetrics.connectionHealthMetrics should not be empty - forAll(currentMetrics) { case (name, value) => - name should startWith("first-config") - value shouldBe 0 // fatal - } - pool.start().futureValueUS.value // the pool startup removes all previous (lingering) metrics and creates new ones @@ -777,6 +771,51 @@ class SequencerConnectionPoolImplTest } } } + + "clean up only the metrics associated to a pool" in { + val poolMetrics = new SequencerClientMetrics( + new SequencerClientHistograms(MetricName("test"))(new HistogramInventory()), + NoOpMetricsFactory, + )(MetricsContext.Empty).connectionPool + + def currentMetricsPsids = + poolMetrics.connectionHealthMetrics.map { case (mc, _gauge) => mc.labels.get("psid") }.toSet + def currentMetricsSize = poolMetrics.connectionHealthMetrics.size + + withConnectionPool( + nbConnections = PositiveInt.three, + trustThreshold = PositiveInt.three, + i => mkConnectionAttributes(synchronizerIndex = 1, sequencerIndex = i + 1), + metrics = poolMetrics, // reuse the same metrics + metricsContext = MetricsContext("psid" -> "psid1"), + namePrefix = "first-config", + ) { (pool1, _, _, _) => + pool1.start().futureValueUS.value + + currentMetricsSize shouldBe 3 + currentMetricsPsids shouldBe Set(Some("psid1")) + + withConnectionPool( + nbConnections = PositiveInt.two, + trustThreshold = PositiveInt.two, + i => mkConnectionAttributes(synchronizerIndex = 1, sequencerIndex = i + 1), + metrics = poolMetrics, // reuse the same metrics + metricsContext = MetricsContext("psid" -> "psid2"), + namePrefix = "second-config", + ) { (pool2, _, _, _) => + pool2.start().futureValueUS.value + + currentMetricsSize shouldBe 5 + currentMetricsPsids shouldBe Set(Some("psid1"), Some("psid2")) + } + + currentMetricsSize shouldBe 3 + currentMetricsPsids shouldBe Set(Some("psid1")) + } + + currentMetricsSize shouldBe 0 + currentMetricsPsids shouldBe Set() + } } private def badBootstrapAssertion( diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala index 7c41fb0799..f3411af36d 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/store/db/DbTest.scala @@ -45,7 +45,7 @@ trait DbTest protected def cleanDb(storage: DbStorage)(implicit tc: TraceContext): FutureUnlessShutdown[?] @SuppressWarnings(Array("org.wartremover.warts.Var", "org.wartremover.warts.Null")) - private var setup: DbStorageSetup = _ + protected var setup: DbStorageSetup = _ /** Stores the db storage implementation. Will throw if accessed before the test has started */ protected lazy val storage: DbStorageIdempotency = { diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala index ef2a31b175..0c297585cb 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/processing/TopologyManagerSigningKeyDetectionTest.scala @@ -12,6 +12,7 @@ import com.digitalasset.canton.topology.store.* import com.digitalasset.canton.topology.store.TopologyStoreId.SynchronizerStore import com.digitalasset.canton.topology.store.memory.InMemoryTopologyStore import com.digitalasset.canton.topology.transaction.* +import com.digitalasset.canton.topology.transaction.ParticipantPermission.Submission import com.digitalasset.canton.topology.transaction.TopologyChangeOp.Replace import com.digitalasset.canton.{BaseTest, HasExecutionContext} import org.scalatest.wordspec.AnyWordSpec @@ -62,13 +63,25 @@ class TopologyManagerSigningKeyDetectionTest .futureValueUS detector - .getValidSigningKeysForTransaction(ts(1), dtc_uid1a, None, returnAllValidKeys = false) + .getValidSigningKeysForTransaction( + ts(1), + dtc_uid1a, + None, + returnAllValidKeys = false, + namespacesToSignFor = Seq.empty, + ) .map(_._2) .futureValueUS shouldBe Right(Seq(SigningKeys.key3.fingerprint)) // test getting all valid keys detector - .getValidSigningKeysForTransaction(ts(1), dtc_uid1a, None, returnAllValidKeys = true) + .getValidSigningKeysForTransaction( + ts(1), + dtc_uid1a, + None, + returnAllValidKeys = true, + namespacesToSignFor = Seq.empty, + ) .futureValueUS .value ._2 should contain theSameElementsAs Seq( @@ -94,7 +107,13 @@ class TopologyManagerSigningKeyDetectionTest loggerFactory.assertLoggedWarningsAndErrorsSeq( detector - .getValidSigningKeysForTransaction(ts(2), dtc_uid1a, None, returnAllValidKeys = false) + .getValidSigningKeysForTransaction( + ts(2), + dtc_uid1a, + None, + returnAllValidKeys = false, + namespacesToSignFor = Seq.empty, + ) .map(_._2) .futureValueUS shouldBe Right( Seq(SigningKeys.key1.fingerprint) @@ -112,6 +131,66 @@ class TopologyManagerSigningKeyDetectionTest ) } + "respsect the requested authorization scope" in { + val detector = mk() + + detector.store + .update( + SequencedTime(ts(0)), + EffectiveTime(ts(0)), + removals = Map.empty, + additions = Seq(ns1k1_k1, ns2k2_k2, ns3k3_k3).map(ValidatedTopologyTransaction(_)), + ) + .futureValueUS + + val ptp = PartyToParticipant.tryCreate( + PartyId.tryCreate("alice", ns1), + PositiveInt.one, + Seq( + HostingParticipant(ParticipantId(UniqueIdentifier.tryCreate("p2", ns2)), Submission), + HostingParticipant(ParticipantId(UniqueIdentifier.tryCreate("p3", ns3)), Submission), + ), + ) + + detector + .getValidSigningKeysForTransaction( + ts(1), + TopologyTransaction( + TopologyChangeOp.Replace, + PositiveInt.one, + ptp, + testedProtocolVersion, + ), + None, + namespacesToSignFor = Seq(ns2), + returnAllValidKeys = false, + ) + .map(_._2) + .futureValueUS shouldBe Right(Seq(SigningKeys.key2.fingerprint)) + + // test getting all valid keys + detector + .getValidSigningKeysForTransaction( + ts(1), + TopologyTransaction( + TopologyChangeOp.Replace, + PositiveInt.one, + ptp, + testedProtocolVersion, + ), + None, + namespacesToSignFor = Seq.empty, + returnAllValidKeys = true, + ) + .futureValueUS + .value + ._2 should contain theSameElementsAs Seq( + SigningKeys.key1, + SigningKeys.key2, + SigningKeys.key3, + ).map(_.fingerprint) + } + "resolves decentralized namespace definitions for finding appropriate signing keys" in { val detector = mk() @@ -140,7 +219,13 @@ class TopologyManagerSigningKeyDetectionTest .futureValueUS detector - .getValidSigningKeysForTransaction(ts(1), otk, None, returnAllValidKeys = false) + .getValidSigningKeysForTransaction( + ts(1), + otk, + None, + returnAllValidKeys = false, + namespacesToSignFor = Seq.empty, + ) .futureValueUS .value ._2 should contain theSameElementsAs Seq( @@ -151,7 +236,13 @@ class TopologyManagerSigningKeyDetectionTest ).map(_.fingerprint) detector - .getValidSigningKeysForTransaction(ts(1), otk, None, returnAllValidKeys = true) + .getValidSigningKeysForTransaction( + ts(1), + otk, + None, + returnAllValidKeys = true, + namespacesToSignFor = Seq.empty, + ) .futureValueUS .value ._2 should contain theSameElementsAs Seq( diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala index 41728c80bc..1187a4d9d3 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/DownloadTopologyStateForInitializationServiceTest.scala @@ -12,7 +12,7 @@ import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime import com.digitalasset.canton.topology.store.StoredTopologyTransactions.GenericStoredTopologyTransactions import com.digitalasset.canton.topology.store.TopologyStoreId.SynchronizerStore import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction -import com.digitalasset.canton.{FailOnShutdown, HasActorSystem} +import com.digitalasset.canton.{FailOnShutdown, HasActorSystem, HasExecutionContext} import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.{Sink, Source} import org.scalatest.wordspec.AsyncWordSpec @@ -21,7 +21,8 @@ trait DownloadTopologyStateForInitializationServiceTest extends AsyncWordSpec with TopologyStoreTestBase with FailOnShutdown - with HasActorSystem { + with HasActorSystem + with HasExecutionContext { protected def mkStore( synchronizerId: PhysicalSynchronizerId, @@ -29,7 +30,7 @@ trait DownloadTopologyStateForInitializationServiceTest ): TopologyStore[SynchronizerStore] private val testData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, executionContext) + new TopologyStoreTestData(testedProtocolVersion, loggerFactory) import testData.* val bootstrapTransactions = StoredTopologyTransactions( diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala index cd9abc9f72..746988f9a0 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTest.scala @@ -30,7 +30,7 @@ import com.digitalasset.canton.topology.transaction.TopologyMapping.Code import com.digitalasset.canton.topology.transaction.{TopologyMapping, *} import com.digitalasset.canton.util.MonadUtil import com.digitalasset.canton.version.ProtocolVersion -import com.digitalasset.canton.{FailOnShutdown, HasActorSystem} +import com.digitalasset.canton.{FailOnShutdown, HasActorSystem, HasExecutionContext} import org.apache.pekko.stream.scaladsl.Sink import org.scalatest.Assertion import org.scalatest.wordspec.AsyncWordSpec @@ -40,12 +40,13 @@ trait TopologyStoreTest extends AsyncWordSpec with TopologyStoreTestBase with FailOnShutdown - with HasActorSystem { + with HasActorSystem + with HasExecutionContext { implicit def closeContext: CloseContext private[store] val testData = - new TopologyStoreTestData(testedProtocolVersion, loggerFactory, executionContext) + new TopologyStoreTestData(testedProtocolVersion, loggerFactory) import testData.* private[store] lazy val largeTestSnapshot = { diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala index e0a5e5c7c2..e760154ade 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/topology/store/TopologyStoreTestData.scala @@ -20,6 +20,7 @@ import com.digitalasset.canton.topology.transaction.* import com.digitalasset.canton.topology.transaction.DelegationRestriction.CanSignAllMappings import com.digitalasset.canton.topology.transaction.ParticipantPermission.Submission import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction +import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.version.ProtocolVersion import org.scalatest.Assertions.fail import org.scalatest.concurrent.ScalaFutures.convertScalaFuture @@ -31,15 +32,13 @@ import scala.concurrent.ExecutionContext class TopologyStoreTestData( testedProtocolVersion: ProtocolVersion, loggerFactory: NamedLoggerFactory, - executionContext: ExecutionContext, -) { +)(implicit executionContext: ExecutionContext) { def makeSignedTx[Op <: TopologyChangeOp, M <: TopologyMapping]( mapping: M, op: Op = TopologyChangeOp.Replace, isProposal: Boolean = false, serial: PositiveInt = PositiveInt.one, )(signingKeys: SigningPublicKey*): SignedTopologyTransaction[Op, M] = { - import com.digitalasset.canton.tracing.TraceContext.Implicits.Empty.* val tx = TopologyTransaction( op, serial, @@ -57,7 +56,7 @@ class TopologyStoreTestData( .from( keysWithUsage.toSeq.map { case (keyId, usage) => factory.syncCryptoClient.crypto.privateCrypto - .sign(tx.hash.hash, keyId, usage) + .sign(tx.hash.hash, keyId, usage)(executionContext, TraceContext.empty) .value .onShutdown(fail("shutdown"))( DirectExecutionContext(loggerFactory.getLogger(this.getClass)) @@ -67,7 +66,6 @@ class TopologyStoreTestData( } ) .getOrElse(fail("no keys provided")) - SignedTopologyTransaction.withSignatures[Op, M]( tx, signatures = signatures, diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala index 575307e710..bcf81d5c4d 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/traffic/TrafficPurchasedSubmissionHandlerTest.scala @@ -18,6 +18,7 @@ import com.digitalasset.canton.protocol.messages.{ } import com.digitalasset.canton.protocol.{DynamicSynchronizerParameters, SynchronizerParameters} import com.digitalasset.canton.sequencing.TrafficControlParameters +import com.digitalasset.canton.sequencing.client.SequencerClient.TrafficCostValidator import com.digitalasset.canton.sequencing.client.SequencerClientSend.SendRequestTimestamps import com.digitalasset.canton.sequencing.client.{ SendAsyncClientError, @@ -96,6 +97,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], aggregationRuleCapture.capture(), callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -175,6 +177,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -231,6 +234,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], any[SendCallback], + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -265,6 +269,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) @@ -325,6 +330,7 @@ class TrafficPurchasedSubmissionHandlerTest any[MessageId], any[Option[AggregationRule]], callbackCapture.capture(), + any[TrafficCostValidator], amplify = any[Boolean], useConfirmationResponseAmplificationParameters = eqTo(false), )(any[TraceContext], any[MetricsContext]) diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala index f7f26c70e6..cfc0571763 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/util/GrpcStreamingUtilsTest.scala @@ -3,15 +3,18 @@ package com.digitalasset.canton.util -import com.digitalasset.canton.BaseTest -import io.grpc.stub.StreamObserver +import com.digitalasset.canton.{BaseTest, HasExecutionContext} +import com.google.protobuf.ByteString +import io.grpc.stub.{ServerCallStreamObserver, StreamObserver} import org.scalatest.wordspec.AnyWordSpec -import java.io.ByteArrayInputStream -import java.util.concurrent.atomic.AtomicInteger +import java.io.{ByteArrayInputStream, OutputStream} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} +import java.util.concurrent.{CountDownLatch, TimeUnit} import scala.collection.mutable +import scala.concurrent.{Future, Promise, blocking} -final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest { +final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest with HasExecutionContext { // we need to use the same value as in GrpcStreamingUtils since it's not configurable val defaultChunkSize = GrpcStreamingUtils.defaultChunkSize private def load( @@ -27,6 +30,54 @@ final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest { } def makeRequest(bytes: Array[Byte]): String = new String(bytes) + /** Base test double for [[ServerCallStreamObserver]] with no-op implementations of the + * flow-control and lifecycle callbacks. Tests override only what they care about (typically + * `onNext` and the readiness behavior). + */ + private class TestServerCallStreamObserver[T] extends ServerCallStreamObserver[T] { + override def isReady: Boolean = true + override def setOnReadyHandler(onReadyHandler: Runnable): Unit = () + override def disableAutoInboundFlowControl(): Unit = () + override def request(count: Int): Unit = () + override def setMessageCompression(enable: Boolean): Unit = () + override def setCompression(compression: String): Unit = () + override def isCancelled: Boolean = false + override def setOnCancelHandler(onCancelHandler: Runnable): Unit = () + + override def onNext(value: T): Unit = () + override def onError(t: Throwable): Unit = () + override def onCompleted(): Unit = () + } + + /** Test double whose readiness can be toggled to mimic gRPC flow control. It starts ready by + * default. `setReady` flips readiness synchronously and runs the registered onReady handler, + * while `goNotReadyThenReadyAfter` simulates the transport buffer filling up and then draining + * asynchronously (firing the onReady handler from another thread). + */ + private class ControllableReadyObserver[T](initiallyReady: Boolean = true) + extends TestServerCallStreamObserver[T] { + private val readyRef = new AtomicBoolean(initiallyReady) + @volatile private var onReadyHandler: Runnable = () => () + + override def isReady: Boolean = readyRef.get() + override def setOnReadyHandler(handler: Runnable): Unit = onReadyHandler = handler + + def setReady(value: Boolean): Unit = { + readyRef.set(value) + onReadyHandler.run() + } + + def goNotReadyThenReadyAfter(delayMillis: Long): Unit = { + readyRef.set(false) + val handler = onReadyHandler + val _ = Future { + blocking(Thread.sleep(delayMillis)) + readyRef.set(true) + handler.run() + } + } + } + "streamToServer with InputStream" should { "stream all chunks to the server" in { val data = Array.tabulate[Byte](defaultChunkSize * 2 + 100)(_.toByte) @@ -114,4 +165,314 @@ final class GrpcStreamingUtilsTest extends AnyWordSpec with BaseTest { } } } + + "streamToClient" should { + + val fromByteString: FromByteString[ByteString] = (chunk: ByteString) => chunk + + "deliver chunks incrementally (not all at once after producer finishes)" in { + val chunkSize = 1024 * 64 + val totalChunks = 5 + + val chunkDelivered = Array.fill(totalChunks)(new CountDownLatch(1)) + val proceedWithNext = Array.fill(totalChunks)(new CountDownLatch(1)) + val completed = new CountDownLatch(1) + val chunksReceived = new AtomicInteger(0) + val producerCompleted = new CountDownLatch(1) + + def awaitLatch(latch: CountDownLatch, clue: String): Unit = + withClue(clue)(latch.await(10, TimeUnit.SECONDS) shouldBe true) + + def assertProducerStillRunning(chunkIdx: Int): Unit = + withClue(s"Producer must not have completed when chunk $chunkIdx is delivered") { + producerCompleted.getCount shouldBe 1L + } + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + override def onNext(value: ByteString): Unit = { + val idx = chunksReceived.getAndIncrement() + if (idx < totalChunks) { + chunkDelivered(idx).countDown() + awaitLatch(proceedWithNext(idx), s"Chunk $idx should be allowed to proceed") + } + } + override def onError(t: Throwable): Unit = completed.countDown() + override def onCompleted(): Unit = completed.countDown() + } + + def producer(os: OutputStream): Future[Unit] = Future { + blocking { + val data = new Array[Byte](chunkSize) + for (i <- 0 until totalChunks) { + java.util.Arrays.fill(data, i.toByte) + os.write(data) + os.flush() + awaitLatch(chunkDelivered(i), s"Chunk $i should be delivered before producer continues") + } + producerCompleted.countDown() + } + } + + // Run in background because streamToClient blocks via Await.result + val streamingF = Future { + blocking { + GrpcStreamingUtils.streamToClient( + responseF = producer, + responseObserver = responseObserver, + fromByteString = fromByteString, + chunkSizeO = Some(chunkSize), + ) + } + } + + // Verify first chunk is delivered while producer is still writing + awaitLatch(chunkDelivered(0), "First chunk should be delivered") + assertProducerStillRunning(0) + proceedWithNext(0).countDown() + + // Verify remaining chunks are delivered incrementally + for (i <- 1 until totalChunks) { + awaitLatch(chunkDelivered(i), s"Chunk $i should be delivered") + if (i < totalChunks - 1) assertProducerStillRunning(i) + proceedWithNext(i).countDown() + } + + completed.await(30, TimeUnit.SECONDS) shouldBe true + chunksReceived.get() shouldBe totalChunks + streamingF.futureValue + } + + "wait until observer is ready before calling onNext" in { + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize)((i: Int) => i.toByte) + val context = io.grpc.Context.current().withCancellation() + + val chunksReceived = new AtomicInteger(0) + val calledWhileNotReady = new AtomicInteger(0) + val onNextCalled = new CountDownLatch(1) + + val responseObserver = new ControllableReadyObserver[ByteString](initiallyReady = false) { + override def onNext(value: ByteString): Unit = { + if (!isReady) calledWhileNotReady.incrementAndGet() + chunksReceived.incrementAndGet() + onNextCalled.countDown() + } + } + + val streamingF = + GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + onNextCalled.await(5, TimeUnit.SECONDS) shouldBe false + calledWhileNotReady.get() shouldBe 0 + chunksReceived.get() shouldBe 0 + + responseObserver.setReady(true) + + onNextCalled.await(10, TimeUnit.SECONDS) shouldBe true + chunksReceived.get() shouldBe 1 + calledWhileNotReady.get() shouldBe 0 + + streamingF.futureValue + } + + "complete the future when responseObserver.onNext throws an exception" in { + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize)((i: Int) => i.toByte) + val context = io.grpc.Context.current().withCancellation() + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + override def onNext(value: ByteString): Unit = + throw new RuntimeException("onNext exploded") + } + + val resultF = GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + // If the bug is present, this future never completes and the test times out + resultF.failed.futureValue shouldBe a[RuntimeException] + } + + "handle producer failure gracefully" in { + val completed = new CountDownLatch(1) + val error = new AtomicInteger(0) + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + override def onError(t: Throwable): Unit = { + error.incrementAndGet() + completed.countDown() + } + override def onCompleted(): Unit = completed.countDown() + } + + GrpcStreamingUtils.streamToClient( + responseF = { (_: OutputStream) => + Future.failed(new RuntimeException("producer exploded")) + }, + responseObserver = responseObserver, + fromByteString = fromByteString, + chunkSizeO = Some(defaultChunkSize), + ) + + completed.await(10, TimeUnit.SECONDS) shouldBe true + error.get() shouldBe 1 + } + + "complete the future when observer becomes not-ready right at EOF" in { + // Reproduces a race where scso.isReady transitions to false right after sending a chunk + // and the worker exits the loop without observing EOF. The completion must still be + // signaled once the observer becomes ready again, even if iter.hasNext was never + // queried at the point where the observer was ready. + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize)(_.toByte) + val context = io.grpc.Context.current().withCancellation() + + val responseObserver = new ControllableReadyObserver[ByteString] { + override def onNext(value: ByteString): Unit = + // Simulate flow control: observer goes not-ready right after buffering a chunk, + // then becomes ready again shortly after (as the network drains the buffer). + goNotReadyThenReadyAfter(20) + } + + val resultF = GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + // If the bug is present, this future never completes and the test times out + resultF.futureValue + } + + "complete streamToClient end-to-end with a ServerCallStreamObserver under flow control" in { + // Mirrors RemoteDumpIntegrationTest: a producer writes multiple chunks to a piped + // output stream and a ServerCallStreamObserver simulates flow control by going + // not-ready after each onNext and firing onReadyHandler shortly afterwards. + // Regression test for a hang where the worker exited the send loop with the observer + // not ready and EOF was never signaled. + val chunkSize = 1024 + val totalChunks = 50 + + val onCompletedLatch = new CountDownLatch(1) + val received = new AtomicInteger(0) + + val responseObserver = new ControllableReadyObserver[ByteString] { + override def onNext(value: ByteString): Unit = { + received.incrementAndGet() + // Half the time, simulate the buffer filling up: go not-ready and let it drain + // asynchronously, firing onReadyHandler from another thread. + if (received.get() % 2 == 0) goNotReadyThenReadyAfter(5) + } + + override def onError(t: Throwable): Unit = onCompletedLatch.countDown() + override def onCompleted(): Unit = onCompletedLatch.countDown() + } + + def producer(os: OutputStream): Future[Unit] = Future { + blocking { + val data = new Array[Byte](chunkSize) + for (i <- 0 until totalChunks) { + java.util.Arrays.fill(data, i.toByte) + os.write(data) + } + } + } + + val streamingF = Future { + blocking { + GrpcStreamingUtils.streamToClient( + responseF = producer, + responseObserver = responseObserver, + fromByteString = (chunk: ByteString) => chunk, + chunkSizeO = Some(chunkSize), + ) + } + } + + onCompletedLatch.await(30, TimeUnit.SECONDS) shouldBe true + received.get() shouldBe totalChunks + streamingF.futureValue + } + + "complete the future when the context is cancelled while the observer is not ready" in { + val chunkSize = 8 + val inputData = Array.tabulate[Byte](chunkSize * 4)(_.toByte) + val context = io.grpc.Context.current().withCancellation() + + val onNextCalls = new AtomicInteger(0) + + val workerParkedWhileNotReady = new CountDownLatch(1) + + val responseObserver = new TestServerCallStreamObserver[ByteString] { + // The observer never becomes ready and never invokes the onReady/onCancel handlers + override def isReady: Boolean = { + workerParkedWhileNotReady.countDown() + false + } + + override def onNext(value: ByteString): Unit = { + onNextCalls.incrementAndGet() + () + } + } + + val resultF = GrpcStreamingUtils.streamResponseChunks(context, responseObserver)( + new ByteArrayInputStream(inputData), + chunkSize, + (chunk: ByteString) => chunk, + ) + + workerParkedWhileNotReady.await(10, TimeUnit.SECONDS) shouldBe true + context.cancel(new io.grpc.StatusRuntimeException(io.grpc.Status.CANCELLED)) + + resultF.futureValue + onNextCalls.get() shouldBe 0 + } + + "not propagate InterruptedException when the serving thread is interrupted while awaiting" in { + // Regression test: when the gRPC serving thread is interrupted (e.g. the client cancelled or + // timed out) while finishStream is blocked in Await.result, the InterruptedException must be + // handled internally and not escape. Otherwise it bubbles up to the gRPC request handler and + // is logged as an "unexpected throwable" + val responseObserver = new TestServerCallStreamObserver[ByteString] + + val producerStarted = new CountDownLatch(1) + val producer: OutputStream => Future[Unit] = { _ => + producerStarted.countDown() + Promise[Unit]().future + } + + val thrown = new AtomicReference[Throwable](null) + val finished = new CountDownLatch(1) + val thread = new Thread(() => { + try + GrpcStreamingUtils.streamToClient( + responseF = producer, + responseObserver = responseObserver, + fromByteString = fromByteString, + chunkSizeO = Some(defaultChunkSize), + ) + catch { + case t: Throwable => thrown.set(t) + } finally finished.countDown() + }) + thread.start() + + producerStarted.await(10, TimeUnit.SECONDS) shouldBe true + blocking(Thread.sleep(200)) + thread.interrupt() + + withClue("streamToClient must return after the serving thread is interrupted") { + finished.await(10, TimeUnit.SECONDS) shouldBe true + } + // With the fix, the InterruptedException is handled internally and never propagates. + Option(thrown.get()) shouldBe None + } + } } diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala index fb42c470bb..481a6f44b5 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/util/PekkoUtilTest.scala @@ -970,43 +970,45 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - if (finalSucceeded.isCompleted) - fail("Should not get so far") - else if (thirdFailed.isCompleted) - finalSucceed.future.map { _ => - finalSucceeded.trySuccess(()) - val (sourceQueue, sourceDone) = Source - .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) - .map { elem => - commit(elem._1) - } - .toMat(Sink.ignore)(Keep.both) - .run() - FutureQueueConsumer( - futureQueue = new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - fromExclusive = 0, - ) - } - else if (secondFailed.isCompleted) - thirdFail.future.map { _ => - thirdFailed.trySuccess(()) - throw new Exception("boom") - } - else if (firstFailed.isCompleted) - secondFail.future.map { _ => - secondFailed.trySuccess(()) - throw new Exception("boom") - } - else - firstFail.future.map { _ => - firstFailed.trySuccess(()) - throw new Exception("boom") - }, - initializationKillSwitch = None, + _ => + if (finalSucceeded.isCompleted) + fail("Should not get so far") + else if (thirdFailed.isCompleted) + finalSucceed.future.map { _ => + finalSucceeded.trySuccess(()) + val (sourceQueue, sourceDone) = Source + .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) + .map { elem => + commit(elem._1) + } + .toMat(Sink.ignore)(Keep.both) + .run() + Future.successful( + FutureQueueConsumer( + futureQueue = new PekkoSourceQueueToFutureQueue( + sourceQueue = sourceQueue, + sourceDone = sourceDone, + loggerFactory = loggerFactory, + ), + fromExclusive = 0, + ) + ) + } + else if (secondFailed.isCompleted) + thirdFail.future.map { _ => + thirdFailed.trySuccess(()) + throw new Exception("boom") + } + else if (firstFailed.isCompleted) + secondFail.future.map { _ => + secondFailed.trySuccess(()) + throw new Exception("boom") + } + else + firstFail.future.map { _ => + firstFailed.trySuccess(()) + throw new Exception("boom") + }, ) recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false firstFail.trySuccess(()) @@ -1040,8 +1042,7 @@ class PekkoUtilTest retryAttemptErrorThreshold = 200, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = _ => consumerPromise.future, - initializationKillSwitch = None, + consumerFactory = _ => _ => consumerPromise.future.map(Future.successful(_)), ) Threading.sleep(10) recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false @@ -1051,6 +1052,52 @@ class PekkoUtilTest recoveringQueue.firstSuccessfulConsumerInitialization.failed.futureValue } + "two-step initialization: firstSuccessfulConsumerInitialization is set after outer but before inner completes cleanly" in assertAllStagesStopped { + val outerPromise = Promise[Future[FutureQueueConsumer[Int]]]() + val innerPromise = Promise[FutureQueueConsumer[Int]]() + val recoveringQueue = new RecoveringFutureQueueImpl[Int]( + maxBlockedOffer = 1, + bufferSize = 20, + loggerFactory = loggerFactory, + retryStategy = PekkoUtil.exponentialRetryWithCap( + minWait = 2, + multiplier = 2, + cap = 10, + ), + retryAttemptWarnThreshold = 100, + retryAttemptErrorThreshold = 200, + uncommittedWarnTreshold = 100, + recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, + consumerFactory = _ => _ => outerPromise.future, + ) + // Initiate shutdown while the outer Future is still pending. Because + // initialization is in progress, the queue is not yet considered done. + recoveringQueue.shutdown() + Threading.sleep(500) + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + recoveringQueue.done.isCompleted shouldBe false + + // Resolve outer so firstSuccessfulConsumerInitialization fires. + outerPromise.success(innerPromise.future) + // give some time for the firstSuccessfulConsumerInitialization to react to the outer promise completion + Threading.sleep(500) + recoveringQueue.firstSuccessfulConsumerInitialization.futureValue + // The inner Future is still pending, so the queue is still not done. + recoveringQueue.done.isCompleted shouldBe false + + innerPromise.success( + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = Future.successful(Done) + override def shutdown(): Unit = () + override def done: Future[Done] = Future.successful(Done) + }, + fromExclusive = 0, + ) + ) + recoveringQueue.done.futureValue + } + "block offer if buffer is full" in assertAllStagesStopped { val received = new AtomicReference[Vector[(Long, Int)]](Vector.empty) val offerGated = Promise[Unit]() @@ -1068,25 +1115,25 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = _ => - Future { - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.map { _ => - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Done - } + _ => + Future.successful(Future { + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.map { _ => + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Done + } - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + override def shutdown(): Unit = shutdownPromise.trySuccess(()) + + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1129,8 +1176,7 @@ class PekkoUtilTest retryAttemptErrorThreshold = 200, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = _ => consumerPromise.future, - initializationKillSwitch = None, + consumerFactory = _ => _ => consumerPromise.future.map(Future.successful(_)), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1179,20 +1225,20 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = _ => - Future { - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - override def offer(elem: (Long, Int)): Future[Done] = - Future.successful(Done) + _ => + Future.successful(Future { + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = + Future.successful(Done) - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = donePromise.future.map(_ => Done) - }, - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = donePromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + }), ) recoveringQueue.firstSuccessfulConsumerInitialization.futureValue shutdownPromise.isCompleted shouldBe false @@ -1225,20 +1271,22 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = _ => - initializedPromise.future.map { _ => - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - override def offer(elem: (Long, Int)): Future[Done] = - Future.successful(Done) + _ => + initializedPromise.future.map { _ => + Future.successful( + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = + Future.successful(Done) - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = donePromise.future.map(_ => Done) - }, - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = donePromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + ) + }, ) recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false shutdownPromise.isCompleted shouldBe false @@ -1259,7 +1307,7 @@ class PekkoUtilTest "Shutdown initiated" ) logEntries(2).debugMessage should include( - "Consumer initialization is in progress, delaying shutdown" + "Consumer initialization is in progress, shutdown signal will be propagated to consumer" ) }, ) @@ -1280,13 +1328,79 @@ class PekkoUtilTest shutdownPromise.isCompleted shouldBe false initializedPromise.trySuccess(()) shutdownPromise.future.futureValue - recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe true recoveringQueue.done.isCompleted shouldBe false Threading.sleep(10) - recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe true recoveringQueue.done.isCompleted shouldBe false donePromise.trySuccess(()) - recoveringQueue.firstSuccessfulConsumerInitialization.failed.futureValue + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe true + recoveringQueue.done.futureValue + } + + "propagate shutdown signal while initialization is in progress" in assertAllStagesStopped { + val initializedPromise = Promise[Unit]() + val shutdownPromise = Promise[Unit]() + val donePromise = Promise[Unit]() + val isShuttingDownObserved = Promise[Unit]() + val recoveringQueue = new RecoveringFutureQueueImpl[Int]( + maxBlockedOffer = 2, + bufferSize = 2, + loggerFactory = loggerFactory, + retryStategy = PekkoUtil.exponentialRetryWithCap( + minWait = 2, + multiplier = 2, + cap = 10, + ), + retryAttemptWarnThreshold = 100, + retryAttemptErrorThreshold = 200, + uncommittedWarnTreshold = 100, + recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, + consumerFactory = _ => + isShuttingDown => + initializedPromise.future.map { _ => + if (isShuttingDown()) isShuttingDownObserved.trySuccess(()) + Future.successful( + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + override def offer(elem: (Long, Int)): Future[Done] = + Future.successful(Done) + + override def shutdown(): Unit = shutdownPromise.trySuccess(()) + + override def done: Future[Done] = donePromise.future.map(_ => Done) + }, + fromExclusive = 0, + ) + ) + }, + ) + recoveringQueue.firstSuccessfulConsumerInitialization.isCompleted shouldBe false + isShuttingDownObserved.isCompleted shouldBe false + // shutdown while consumer initialization is still in progress + loggerFactory.assertEventuallyLogsSeq( + SuppressionRule.LoggerNameContains("RecoveringFutureQueueImpl") && + SuppressionRule.LevelAndAbove(org.slf4j.event.Level.DEBUG) + )( + recoveringQueue.shutdown(), + logEntries => { + logEntries should have size (3) + logEntries.head.infoMessage should include( + "Before shutting down, preventing further initialization retries" + ) + logEntries(1).infoMessage should include( + "Shutdown initiated" + ) + logEntries(2).debugMessage should include( + "Consumer initialization is in progress, shutdown signal will be propagated to consumer" + ) + }, + ) + initializedPromise.trySuccess(()) + isShuttingDownObserved.future.futureValue + shutdownPromise.future.futureValue + donePromise.trySuccess(()) + recoveringQueue.firstSuccessfulConsumerInitialization.futureValue recoveringQueue.done.futureValue } @@ -1305,11 +1419,11 @@ class PekkoUtilTest retryAttemptErrorThreshold = 200, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = _ => { - firstConsumerInitializationFailedPromise.trySuccess(()) - Future.failed(new Exception("boom")) - }, - initializationKillSwitch = None, + consumerFactory = _ => + _ => { + firstConsumerInitializationFailedPromise.trySuccess(()) + Future.failed(new Exception("boom")) + }, ) firstConsumerInitializationFailedPromise.future.futureValue Threading.sleep(10) @@ -1367,14 +1481,13 @@ class PekkoUtilTest retryAttemptErrorThreshold = 6, uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, - consumerFactory = { _ => + consumerFactory = { _ => _ => val f = initializationContinuePromise.get().future.map { _ => throw new Exception("initialization fails") } initializationStartedPromise.get().trySuccess(()) f }, - initializationKillSwitch = None, ) // info 1 initializationStartedPromise.get().future.futureValue @@ -1454,7 +1567,7 @@ class PekkoUtilTest "Shutdown initiated" ) logEntries(2).debugMessage should include( - "Consumer initialization is in progress, delaying shutdown" + "Consumer initialization is in progress, shutdown signal will be propagated to consumer" ) }, ) @@ -1483,36 +1596,36 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.map { _ => - if (elem._2 == 4 && firstConsumer.get()) throw new Exception("boom") - else { - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - commit(elem._1) - Done + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.map { _ => + if (elem._2 == 4 && firstConsumer.get()) throw new Exception("boom") + else { + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + commit(elem._1) + Done + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1559,38 +1672,38 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else { - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - commit(elem._1) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else { + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + commit(elem._1) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1632,45 +1745,45 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else if (elem._2 >= 3 && firstConsumer.get()) { - // forget, not commit - Future.successful(Done) - } else if (elem._2 >= 2 && firstConsumer.get()) { - // not commit - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) - } else { - commit(elem._1) - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else if (elem._2 >= 3 && firstConsumer.get()) { + // forget, not commit + Future.successful(Done) + } else if (elem._2 >= 2 && firstConsumer.get()) { + // not commit + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } else { + commit(elem._1) + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1712,41 +1825,41 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else if (elem._2 >= 2 && firstConsumer.get()) { - // forget, not commit - Future.successful(Done) - } else { - commit(elem._1) - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else if (elem._2 >= 2 && firstConsumer.get()) { + // forget, not commit + Future.successful(Done) + } else { + commit(elem._1) + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1788,42 +1901,42 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) - if (firstConsumer.get()) { - firstConsumer.set(false) - recoveryIndexRef.set(recoveryIndex) - } else { - firstConsumer.set(true) - } - FutureQueueConsumer( - futureQueue = new FutureQueue[(Long, Int)] { - private val shutdownPromise = Promise[Unit]() - - override def offer(elem: (Long, Int)): Future[Done] = - offerGated.future.flatMap { _ => - if (elem._2 == 4 && firstConsumer.get()) { - shutdownPromise.tryFailure(new Exception("delegate boom")) - Future.never - } else if (elem._2 >= 2 && firstConsumer.get()) { - // forget, but commit: very wrong - commit(elem._1) - Future.successful(Done) - } else { - commit(elem._1) - discard(received.accumulateAndGet(Vector(elem), _ ++ _)) - Future.successful(Done) + _ => + Future.successful(Future { + val recoveryIndex = received.get().lastOption.map(_._1).getOrElse(0L) + if (firstConsumer.get()) { + firstConsumer.set(false) + recoveryIndexRef.set(recoveryIndex) + } else { + firstConsumer.set(true) + } + FutureQueueConsumer( + futureQueue = new FutureQueue[(Long, Int)] { + private val shutdownPromise = Promise[Unit]() + + override def offer(elem: (Long, Int)): Future[Done] = + offerGated.future.flatMap { _ => + if (elem._2 == 4 && firstConsumer.get()) { + shutdownPromise.tryFailure(new Exception("delegate boom")) + Future.never + } else if (elem._2 >= 2 && firstConsumer.get()) { + // forget, but commit: very wrong + commit(elem._1) + Future.successful(Done) + } else { + commit(elem._1) + discard(received.accumulateAndGet(Vector(elem), _ ++ _)) + Future.successful(Done) + } } - } - override def shutdown(): Unit = shutdownPromise.trySuccess(()) + override def shutdown(): Unit = shutdownPromise.trySuccess(()) - override def done: Future[Done] = shutdownPromise.future.map(_ => Done) - }, - fromExclusive = recoveryIndex, - ) - }, - initializationKillSwitch = None, + override def done: Future[Done] = shutdownPromise.future.map(_ => Done) + }, + fromExclusive = recoveryIndex, + ) + }), ) recoveringQueue.offer(1).futureValue recoveringQueue.offer(2).futureValue @@ -1862,38 +1975,38 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - if (sleepy) Threading.sleep(Random.nextLong(2)) - if (Random.nextLong(6) > 0) throw new Exception("initialization boom") - val (sourceQueue, sourceDone) = Source - .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) - .via(BatchN(5, 3)) - .mapAsync(3) { batch => - Future { - if (sleepy) Threading.sleep(Random.nextLong(4) / 4) - if (Random.nextLong(10) == 0) { - boomCount.incrementAndGet() - throw new Exception("boom") + _ => + Future.successful(Future[FutureQueueConsumer[Int]] { + if (sleepy) Threading.sleep(Random.nextLong(2)) + if (Random.nextLong(6) > 0) throw new Exception("initialization boom") + val (sourceQueue, sourceDone) = Source + .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) + .via(BatchN(5, 3)) + .mapAsync(3) { batch => + Future { + if (sleepy) Threading.sleep(Random.nextLong(4) / 4) + if (Random.nextLong(10) == 0) { + boomCount.incrementAndGet() + throw new Exception("boom") + } + batch } - batch } - } - .map { batch => - batch.foreach(elem => sink.getAndUpdate(elem :: _)) - commit(batch.last._1) - } - .toMat(Sink.ignore)(Keep.both) - .run() - FutureQueueConsumer( - futureQueue = new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - fromExclusive = sink.get().headOption.map(_._1).getOrElse(0), - ) - }, - initializationKillSwitch = None, + .map { batch => + batch.foreach(elem => sink.getAndUpdate(elem :: _)) + commit(batch.last._1) + } + .toMat(Sink.ignore)(Keep.both) + .run() + FutureQueueConsumer( + futureQueue = new PekkoSourceQueueToFutureQueue( + sourceQueue = sourceQueue, + sourceDone = sourceDone, + loggerFactory = loggerFactory, + ), + fromExclusive = sink.get().headOption.map(_._1).getOrElse(0), + ) + }), ) val testF = Future { val inputFixture = Iterator.iterate(1)(_ + 1).take(inputSize).toList @@ -1970,30 +2083,30 @@ class PekkoUtilTest uncommittedWarnTreshold = 100, recoveringQueueMetrics = RecoveringQueueMetrics.NoOp, consumerFactory = commit => - Future { - val (sourceQueue, sourceDone) = Source - .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) - .via(BatchN(5, 3)) - .mapAsync(3) { batch => - Future { - batch + _ => + Future.successful(Future { + val (sourceQueue, sourceDone) = Source + .queue[(Long, Int)](20, OverflowStrategy.backpressure, 1) + .via(BatchN(5, 3)) + .mapAsync(3) { batch => + Future { + batch + } } - } - .map { batch => - commit(batch.last._1) - } - .toMat(Sink.ignore)(Keep.both) - .run() - FutureQueueConsumer( - futureQueue = new PekkoSourceQueueToFutureQueue( - sourceQueue = sourceQueue, - sourceDone = sourceDone, - loggerFactory = loggerFactory, - ), - fromExclusive = 0, - ) - }, - initializationKillSwitch = None, + .map { batch => + commit(batch.last._1) + } + .toMat(Sink.ignore)(Keep.both) + .run() + FutureQueueConsumer( + futureQueue = new PekkoSourceQueueToFutureQueue( + sourceQueue = sourceQueue, + sourceDone = sourceDone, + loggerFactory = loggerFactory, + ), + fromExclusive = 0, + ) + }), ) val start = System.nanoTime() Iterator diff --git a/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala b/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala index f25848a7ef..e5f5d165f4 100644 --- a/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala +++ b/canton/community/common/src/test/scala/com/digitalasset/canton/util/SimpleExecutionQueueTest.scala @@ -6,6 +6,7 @@ package com.digitalasset.canton.util import com.digitalasset.canton.lifecycle.UnlessShutdown.{AbortedDueToShutdown, Outcome} import com.digitalasset.canton.lifecycle.{FutureUnlessShutdown, UnlessShutdown} import com.digitalasset.canton.logging.{LogEntry, SuppressionRule} +import com.digitalasset.canton.util.FailureMode.{ContinueAfterFailure, StopAfterFailure} import com.digitalasset.canton.{BaseTest, HasExecutionContext, config} import org.scalatest.BeforeAndAfterEach import org.scalatest.wordspec.AsyncWordSpec diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala index 066cc92a03..792dff6f90 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/continuity/ProtocolContinuityConformanceTest.scala @@ -36,8 +36,6 @@ import com.digitalasset.canton.version.{ProtocolVersion, ReleaseVersion} import monocle.macros.syntax.lens.* import org.scalatest.concurrent.PatienceConfiguration -import scala.concurrent.duration.DurationInt - trait MultiVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { protected def testedReleases: List[TestedRelease] @@ -96,7 +94,7 @@ trait MultiVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { "ExplicitDisclosureIT:EDDuplicates", ) else Seq.empty - LedgerApiConformanceBase.excludedTests ++ perReleaseExclusions + perReleaseExclusions ++ LedgerApiConformanceBase.excludedTests(testedProtocolVersion) } } @@ -169,7 +167,7 @@ trait ProtocolContinuityConformanceTestSynchronizer extends ProtocolContinuityCo testedReleases.foreach { case TestedRelease(release, protocolVersions) => lazy val binDir = ReleaseUtils .retrieve(release) - .futureValue(timeout = PatienceConfiguration.Timeout(2.minutes)) + .futureValue(PatienceConfiguration.Timeout(ReleaseUtils.DefaultReleaseDownloadTimeout)) lazy val pv = protocolVersions.max1 s"run conformance tests of shard $shard with release $release and protocol $pv" in { @@ -245,7 +243,7 @@ trait ProtocolContinuityConformanceTestParticipant extends ProtocolContinuityCon testedReleases.foreach { case TestedRelease(release, protocolVersions) => lazy val binDir = ReleaseUtils .retrieve(release) - .futureValue(timeout = PatienceConfiguration.Timeout(2.minutes)) + .futureValue(PatienceConfiguration.Timeout(ReleaseUtils.DefaultReleaseDownloadTimeout)) lazy val pv = protocolVersions.max1 s"run conformance tests of shard $shard with release $release and protocol $pv" in { @@ -316,7 +314,7 @@ trait ProtocolContinuityConformanceTestPing extends ProtocolContinuityConformanc testedReleases.foreach { case TestedRelease(release, protocolVersions) => lazy val binDir = ReleaseUtils .retrieve(release) - .futureValue(timeout = PatienceConfiguration.Timeout(2.minutes)) + .futureValue(PatienceConfiguration.Timeout(ReleaseUtils.DefaultReleaseDownloadTimeout)) lazy val pv = protocolVersions.max1 s"ping between current-branch participant and release $release participant (pv=$pv)" in { @@ -436,11 +434,11 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.ledger-api.topology-aware-package-selection.max-passes-default", s"$base.ledger-api.topology-aware-package-selection.max-passes-limit", s"$base.ledger-api.update-service", - s"$base.parameters.alpha-multi-synchronizer-support", s"$base.parameters.caching.bft-ordering-batch-cache", s"$base.parameters.caching.sequencer-catchup-payload-cache", s"$base.parameters.commit-after-failed-activeness-check", s"$base.parameters.commitment-use-db-snapshot-for-participant-lookup", + s"$base.parameters.enable-all-ledger-api-reassignments", s"$base.parameters.validate-legacy-contracts-v-11", s"$base.parameters.ledger-api-server.indexer.achs-config", s"$base.parameters.ledger-api-server.indexer.postgres-data-source", @@ -452,6 +450,8 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.sequencer-client.channel-max-inbound-message-size", s"$base.sequencer-client.keep-alive-client.idle-timeout", s"$base.sequencer-client.keep-alive-client.keep-alive-without-calls", + s"$base.parameters.connect-to-synchronizers-on-startup", + s"$base.traffic-enforcement", ) } val perMediator = { @@ -464,6 +464,7 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.crypto.session-signing-keys", s"$base.parameters.caching.bft-ordering-batch-cache", s"$base.parameters.caching.sequencer-catchup-payload-cache", + s"$base.parameters.delayed-verdict-sender", s"$base.sequencer-client.amplify-on-max-sequencing-time-too-far", s"$base.sequencer-client.channel-flow-control-window", s"$base.sequencer-client.channel-max-inbound-message-size", @@ -488,6 +489,8 @@ private[continuity] object ProtocolContinuityConformanceTest { s"$base.parameters.unsafe-sequencer-channel-support", // Once we remove PV34, we can remove this exception s"$base.parameters.disable-release-version-handshake-check", + s"$base.parameters.enable-prevalidation", + s"$base.parameters.enable-reject-delivered-aggregations-on-pv-35", s"$base.public-api.max-concurrent-calls-per-connection", s"$base.sequencer-client.amplify-on-max-sequencing-time-too-far", s"$base.sequencer-client.channel-flow-control-window", diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala index 1c2e50a53c..7c4859c3b5 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/ExcludedTests.scala @@ -12,16 +12,21 @@ import com.daml.ledger.api.testtool.runner.AvailableTests object ExcludedTests { /** Suites excluded when running via JSON API (service-level unsupported operations) */ - val jsonApiExcludedSuites: Seq[String] = Seq( + val jsonApiExcludedTests: Seq[String] = Seq( // health service not available in JSON API "HealthServiceIT", // updatePartyIdentityProviderId not available; getParties fails with empty party list "PartyManagementServiceIT", // PERMISSION_DENIED due to user-management auth mismatch in JSON API "UserManagementServiceIT", + // TODO(#33111): Testing wrongly typed contract keys is not supported for JSON API + "PrefetchContractKeysIT:CSprefetchContractKeysPrepareWronglyTyped", + "PrefetchContractKeysIT:CSprefetchContractKeysWronglyTyped", + // TODO(#27501): Remove the exclusion once JSON API supports Commands.prefetchContractKeys + "PrefetchContractKeysIT", ) - lazy val grpcOnlyTestNames: Seq[String] = AvailableTests.v2_2 + lazy val grpcOnlyTestNames: Seq[String] = AvailableTests.latestStableLf .defaultTests(timeoutScaleFactor = 1.0) .flatMap(_.tests) .collect { @@ -35,5 +40,5 @@ object ExcludedTests { // On the other hand we might occasionally have tests only in previous release lines, that current main does not know about // So ideally we need to have exclude using current code plus test tools def findExcludedTests(useJson: Boolean): Seq[String] = - if (useJson) grpcOnlyTestNames ++ jsonApiExcludedSuites else Seq.empty + if (useJson) grpcOnlyTestNames ++ jsonApiExcludedTests else Seq.empty } diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala index a5e77c5e97..3be6bcc066 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/JsonApiConformanceIntegrationTest.scala @@ -65,11 +65,12 @@ sealed trait JsonApiConformanceBase concurrentTestRuns = 4, // these tests run together with all other tests connectedSynchronizers = env.environment.config.sequencers.size, ) - val availableTests = AvailableTests.v2_2 + + val availableTests = AvailableTests.testsForProtocol(testedProtocolVersion) val envArgInclusion = envArgTestsInclusion.getOrElse(TestInclusions.AllIncluded) val testsToRun = - new ConfiguredTests(availableTests, config).defaultTests.view + ConfiguredTests(availableTests, config).defaultTests.view .flatMap(_.tests) .filter { testCase => testCase.limitation match { @@ -182,7 +183,7 @@ sealed abstract class JsonApiConformanceIntegrationShardedTest( override def environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P3_S1M1_S1M1 .prependConfigTransform(ConfigTransforms.enableHttpLedgerApi) - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup { implicit env => import env.* participants.all.synchronizers.connect_local(sequencer1, alias = daName) @@ -191,8 +192,10 @@ sealed abstract class JsonApiConformanceIntegrationShardedTest( .withTrafficControl(TestUtils.waitForTargetTimeOnSynchronizerNode(wallClock.now, logger)) protected def inclusions: TestInclusions = TestInclusions.AllIncluded - override protected def exclusions: Set[String] = LedgerApiConformanceBase.excludedTests.toSet ++ - ExcludedTests.jsonApiExcludedSuites.toSet + override protected def exclusions: Set[String] = + ExcludedTests.jsonApiExcludedTests.toSet ++ LedgerApiConformanceBase.excludedTests( + testedProtocolVersion + ) protected def testCaseName = "pass the Ledger API conformance tests" } diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala index 914e710e22..1e59ba1996 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/ledgerapi/LedgerApiConformanceTest.scala @@ -3,6 +3,7 @@ package com.digitalasset.canton.integration.tests.ledgerapi +import com.daml.ledger.api.testtool.runner.AvailableTests import com.digitalasset.canton.config import com.digitalasset.canton.config.* import com.digitalasset.canton.config.CantonRequireTypes.InstanceName @@ -11,7 +12,6 @@ import com.digitalasset.canton.integration.ConfigTransforms.updateAllParticipant import com.digitalasset.canton.integration.plugins.* import com.digitalasset.canton.integration.plugins.UseLedgerApiTestTool.LAPITTVersion import com.digitalasset.canton.integration.plugins.UseReferenceBlockSequencer.MultiSynchronizer -import com.digitalasset.canton.integration.tests.ledgerapi.LedgerApiConformanceBase.excludedTests import com.digitalasset.canton.integration.tests.ledgerapi.SuppressionRules.ApiUserManagementServiceSuppressionRule import com.digitalasset.canton.integration.util.TestUtils import com.digitalasset.canton.integration.{ @@ -29,7 +29,8 @@ import monocle.macros.syntax.lens.* import org.slf4j.event trait SingleVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { - protected def lfVersion: LanguageVersion = LanguageVersion.v2_2 + protected def lfVersion: LanguageVersion = + AvailableTests.testsForProtocol(testedProtocolVersion).lfVersion protected def lapittVersion: LAPITTVersion = LAPITTVersion.Local @@ -48,7 +49,7 @@ trait SingleVersionLedgerApiConformanceBase extends LedgerApiConformanceBase { ledgerApiTestToolPlugin.runShardedSuites( shard, numShards, - exclude = excludedTests, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion), useJson = false, )(env) } @@ -109,7 +110,7 @@ class LedgerApiConformanceMultiSynchronizerTest override lazy val environmentDefinition: EnvironmentDefinition = EnvironmentDefinition.P2_S1M1_S1M1 - .addConfigTransforms(ConfigTransforms.enableAlphaMultiSynchronizerTopologyFeatureFlag) + .addConfigTransforms(ConfigTransforms.enableMultiSynchronizerTopologyFeatureFlag) .withSetup(setupLedgerApiConformanceEnvironment) // ensure ledger api conformance tests have less noisy neighbours @@ -129,7 +130,7 @@ class LedgerApiConformanceMultiSynchronizerTest new UseLedgerApiTestTool( loggerFactory, connectedSynchronizersCount = connectedSynchronizersCount, - lfVersion = LanguageVersion.v2_2, + lfVersion = AvailableTests.testsForProtocol(testedProtocolVersion).lfVersion, version = LAPITTVersion.Local, ) registerPlugin(new UsePostgres(loggerFactory)) @@ -167,7 +168,7 @@ object LedgerApiConformanceBase { "VettingIT:PVListVettedPackagesMultiSynchronizer", "VettingIT:PVListVettedPackagesPagination", ) - val excludedTests = Seq( + private val disabledTests = Seq( // Exclude tests which are run separately below "ParticipantPruningIT", "TLSOnePointThreeIT", @@ -193,7 +194,36 @@ object LedgerApiConformanceBase { "CommandServiceIT:CSRefuseBadParameter", // TODO(i31186): enable this once the issue is fixed "TransactionServiceVisibilityIT:TXLedgerEffectsHideCommandIdToNonSubmittingStakeholders", + // TODO(#33111): Test is disabled because it generates multiple command root nodes for the prepare endpoint, which is currently not supported + "PrefetchContractKeysIT:CSprefetchContractPrepareKeysMany", + // TODO(#33111): The tests below were written with UCK semantics in mind and are generally considered broken now. + // They should be checked up one by one and either fixed or removed. For now, they are excluded to unblock general testing of LF 2.3 + // tests with divulged/disclosed contracts fail on Canton as does scoping by maintainer unless we're on a UCK synchronizer (see below) + "ContractKeysIT:CKFetchOrLookup", + "ContractKeysIT:CKMaintainerScoped", + "ContractKeysIT:CKNoFetchUndisclosed", + // tests with unique contract key assumption fail as does RWArchiveVsFailedLookupByKey (finding a lookup failure after contract creation) + "RaceConditionIT:RWArchiveVsFailedLookupByKey", + "RaceConditionIT:WWArchiveVsNonTransientCreate", + "RaceConditionIT:WWDoubleNonTransientCreate", + "RaceConditionIT:RWTransientCreateVsNonTransientCreate", ) + + private val excludedTestsForPV34 = Seq( + // Package dependency unvetting becomes supported starting with PV35 + "VettingIT:PVUnvettedDependenciesSupported" + ) + + private val excludedTestsForPV35AndAbove = Seq( + // Test disabled due to package dependency unvetting becoming supported starting with PV35 + "VettingIT:PVCheckUnvettedPackagesExceptWithForceFlag" + ) + + def excludedTests(testedProtocolVersion: ProtocolVersion): Seq[String] = + disabledTests ++ { + if (testedProtocolVersion <= ProtocolVersion.v34) excludedTestsForPV34 + else excludedTestsForPV35AndAbove + } } abstract class LedgerApiShardedConformanceBase(shard: Int) diff --git a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala index e787769d05..69a73f8c28 100644 --- a/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala +++ b/canton/community/conformance-testing/src/test/scala/com/digitalasset/canton/integration/tests/variations/LedgerApiVariationsConformanceTest.scala @@ -7,9 +7,11 @@ import com.daml.tls.{TlsServerConfig, TlsVersion} import com.digitalasset.canton.config.* import com.digitalasset.canton.config.RequireTypes.ExistingFile import com.digitalasset.canton.integration.plugins.* -import com.digitalasset.canton.integration.tests.ledgerapi.LedgerApiConformanceBase.excludedTests -import com.digitalasset.canton.integration.tests.ledgerapi.SingleVersionLedgerApiConformanceBase import com.digitalasset.canton.integration.tests.ledgerapi.SuppressionRules.ApiUserManagementServiceSuppressionRule +import com.digitalasset.canton.integration.tests.ledgerapi.{ + LedgerApiConformanceBase, + SingleVersionLedgerApiConformanceBase, +} import com.digitalasset.canton.integration.util.TestUtils import com.digitalasset.canton.integration.{ConfigTransforms, EnvironmentDefinition} import com.digitalasset.canton.logging.SuppressionRule @@ -52,7 +54,7 @@ sealed abstract class LedgerApiInMemoryFanOutConformanceTestShardedPostgres(shar ledgerApiTestToolPlugin.runShardedSuites( shard = shard, numShards = numShards, - exclude = excludedTests, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion), concurrentTestRuns = VariationsConformanceTestUtils.ConcurrentTestRuns, useJson = false, ) @@ -120,7 +122,7 @@ sealed abstract class LedgerApiTinyBuffersConformanceShardedTestPostgres(shard: ledgerApiTestToolPlugin.runShardedSuites( shard = shard, numShards = numShards, - exclude = excludedTests, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion), concurrentTestRuns = VariationsConformanceTestUtils.ConcurrentTestRuns, useJson = false, ) @@ -256,7 +258,7 @@ trait LedgerApiStaticTimeConformanceTest extends SingleVersionLedgerApiConforman ledgerApiTestToolPlugin.runShardedSuites( shard = 0, numShards = 1, - exclude = excludedTests ++ exclusions, + exclude = LedgerApiConformanceBase.excludedTests(testedProtocolVersion) ++ exclusions, concurrentTestRuns = VariationsConformanceTestUtils.ConcurrentTestRuns, useJson = false, ) diff --git a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala index 20fce5166d..f5199ee091 100644 --- a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala +++ b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala @@ -706,7 +706,7 @@ class Engine( // token's buffer before going back to the caller. def wrapHasStarted( - overflow: Vector[FatContractInstance], + overflow: Vector[ResultNeedKey.Response.ContractEntry], callerProgression: NeedKeyProgression.HasStarted, ): NeedKeyProgression.HasStarted = if (overflow.nonEmpty) @@ -723,19 +723,34 @@ class Engine( NeedKeyProgression.Finished } + def resumeWithNeededEntries( + entries: Vector[ResultNeedKey.Response.ContractEntry], + callerHasStarted: NeedKeyProgression.HasStarted, + ) = { + val (enginePage, engineRest) = entries.splitAt(n) + val callerFcis = enginePage.map { + case ResultNeedKey.Response.AuthenticableFatContractInstance( + contractInstance, + _, + _, + ) => + contractInstance + case ResultNeedKey.Response.UnsupportedContractIdVersion(_) => + throw new NotImplementedError( + "UnsupportedContractIdVersion is not yet supported." + ) + } + callback(callerFcis, wrapHasStarted(engineRest, callerHasStarted)) + interpretLoop(machine, time, submissionInfo) + } + def askCaller(callerToken: NeedKeyProgression.CanContinue) = ResultNeedKey( gk, n, callerToken, - { - ( - callerContracts: Vector[FatContractInstance], - callerHasStarted: NeedKeyProgression.HasStarted, - ) => - val (enginePage, engineRest) = callerContracts.splitAt(n) - callback(enginePage, wrapHasStarted(engineRest, callerHasStarted)) - interpretLoop(machine, time, submissionInfo) + { case ResultNeedKey.Response(callerContracts, callerHasStarted) => + resumeWithNeededEntries(callerContracts, callerHasStarted) }, ) @@ -746,9 +761,7 @@ class Engine( if (overflow.nonEmpty) { // We have buffered contracts from a previous caller response. // Serve from the buffer without asking the caller. - val (page, rest) = overflow.splitAt(n) - callback(page, wrapHasStarted(rest, callerProgression)) - interpretLoop(machine, time, submissionInfo) + resumeWithNeededEntries(overflow, callerProgression) } else { // Empty buffer โ€” unwrap the caller progression. callerProgression match { @@ -1110,7 +1123,7 @@ object Engine { Error.Interpretation(Error.Interpretation.DamlException(error), None) private final case class BufferedKeyContracts( - overflow: Vector[FatContractInstance], + overflow: Vector[ResultNeedKey.Response.ContractEntry], callerProgression: NeedKeyProgression.HasStarted, ) extends NeedKeyProgression.Token diff --git a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala index 641a3e1cff..ce8dbe13e7 100644 --- a/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala +++ b/canton/community/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala @@ -6,12 +6,13 @@ package engine import cats.Applicative import com.digitalasset.daml.lf.crypto.Hash -import com.digitalasset.daml.lf.data.Ref._ +import com.digitalasset.daml.lf.data.Ref.* import com.digitalasset.daml.lf.data.{BackStack, FrontStack, ImmArray} import com.digitalasset.daml.lf.engine.ResultNeedContract.Response -import com.digitalasset.daml.lf.language.Ast._ +import com.digitalasset.daml.lf.engine.ResultNeedKey.Response.AuthenticableFatContractInstance +import com.digitalasset.daml.lf.language.Ast.* import com.digitalasset.daml.lf.transaction.{FatContractInstance, GlobalKey, NeedKeyProgression} -import com.digitalasset.daml.lf.value.Value._ +import com.digitalasset.daml.lf.value.Value.* import scalaz.Monad import scala.annotation.tailrec @@ -31,7 +32,7 @@ sealed trait Result[+A] extends Product with Serializable { case ResultNeedPackage(pkgId, resume) => ResultNeedPackage(pkgId, mbPkg => resume(mbPkg).map(f)) case ResultNeedKey(gk, limit, token, resume) => - ResultNeedKey(gk, limit, token, (cids, token) => resume(cids, token).map(f)) + ResultNeedKey(gk, limit, token, response => resume(response).map(f)) case ResultPrefetch(contractIds, keys, resume) => ResultPrefetch(contractIds, keys, () => resume().map(f)) } @@ -50,7 +51,7 @@ sealed trait Result[+A] extends Product with Serializable { gk, limit, token, - (mbAcoid, nextToken) => resume(mbAcoid, nextToken).flatMap(f), + response => resume(response).flatMap(f), ) case ResultPrefetch(contractIds, keys, resume) => ResultPrefetch(contractIds, keys, () => resume().flatMap(f)) @@ -78,7 +79,24 @@ sealed trait Result[+A] extends Product with Serializable { })) case ResultNeedPackage(pkgId, resume) => go(resume(pkgs.lift(pkgId))) case ResultNeedKey(key, _, _, resume) => - go(resume(keys.lift(key).getOrElse(Vector.empty), NeedKeyProgression.Finished)) + go( + resume( + ResultNeedKey.Response( + keys + .lift(key) + .getOrElse(Vector.empty) + .map(fci => + AuthenticableFatContractInstance( + fci, + hashingMethod(fci.contractId), + hash => idValidator(fci.contractId, hash), + ) + ), + NeedKeyProgression.Finished, + ) + ) + ) + case ResultPrefetch(_, _, result) => go(result()) } go(this) @@ -165,9 +183,9 @@ final case class ResultNeedPackage[A](packageId: PackageId, resume: Option[Packa extends Result[A] /** Intermediate result indicating that contracts matching a key are required to complete the computation. - * To resume the computation, the caller must invoke `resume` with the following arguments: + * To resume the computation, the caller must invoke `resume` with a page containting the following information: *

|^d8*xHg5w8#u7raF|0giw0JVK;@ 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