mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
update canton to 20240419.13141.v8e75f5c1 (#19057)
* update canton to 20240419.13141.v8e75f5c1 tell-slack: canton * fix the compilation of community_ledger_ledger-json-api --------- Co-authored-by: Azure Pipelines Daml Build <support@digitalasset.com> Co-authored-by: Paul Brauner <paul.brauner@digitalasset.com>
This commit is contained in:
parent
af57809e3d
commit
612c9bdd74
@ -720,6 +720,8 @@ scala_library(
|
|||||||
"@maven//:org_apache_pekko_pekko_actor_2_13",
|
"@maven//:org_apache_pekko_pekko_actor_2_13",
|
||||||
"@maven//:org_apache_pekko_pekko_http_2_13",
|
"@maven//:org_apache_pekko_pekko_http_2_13",
|
||||||
"@maven//:org_apache_pekko_pekko_http_core_2_13",
|
"@maven//:org_apache_pekko_pekko_http_core_2_13",
|
||||||
|
"@maven//:org_bouncycastle_bcpkix_jdk15on",
|
||||||
|
"@maven//:org_bouncycastle_bcprov_jdk15on",
|
||||||
"@maven//:org_scala_lang_scala_reflect",
|
"@maven//:org_scala_lang_scala_reflect",
|
||||||
"@maven//:org_scalaz_scalaz_core_2_13",
|
"@maven//:org_scalaz_scalaz_core_2_13",
|
||||||
"@maven//:org_slf4j_slf4j_api",
|
"@maven//:org_slf4j_slf4j_api",
|
||||||
|
@ -23,6 +23,9 @@ service PackageService {
|
|||||||
// Upload a DAR file and all packages inside to the participant node
|
// Upload a DAR file and all packages inside to the participant node
|
||||||
rpc UploadDar(UploadDarRequest) returns (UploadDarResponse);
|
rpc UploadDar(UploadDarRequest) returns (UploadDarResponse);
|
||||||
|
|
||||||
|
// Upload a DAR file and all packages inside to the participant node
|
||||||
|
rpc ValidateDar(ValidateDarRequest) returns (ValidateDarResponse);
|
||||||
|
|
||||||
// Remove a package that is not vetted
|
// Remove a package that is not vetted
|
||||||
rpc RemovePackage(RemovePackageRequest) returns (RemovePackageResponse);
|
rpc RemovePackage(RemovePackageRequest) returns (RemovePackageResponse);
|
||||||
|
|
||||||
@ -139,6 +142,17 @@ message UploadDarResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs the same checks that UploadDarRequest would perform, but doesn't
|
||||||
|
// upload the DAR.
|
||||||
|
message ValidateDarRequest {
|
||||||
|
bytes data = 1;
|
||||||
|
string filename = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ValidateDarResponse {
|
||||||
|
string hash = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetDarRequest {
|
message GetDarRequest {
|
||||||
string hash = 1;
|
string hash = 1;
|
||||||
}
|
}
|
||||||
|
@ -151,6 +151,42 @@ object ParticipantAdminCommands {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final case class ValidateDar(
|
||||||
|
darPath: Option[String],
|
||||||
|
logger: TracedLogger,
|
||||||
|
) extends PackageCommand[ValidateDarRequest, ValidateDarResponse, String] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, ValidateDarRequest] =
|
||||||
|
for {
|
||||||
|
pathValue <- darPath.toRight("DAR path not provided")
|
||||||
|
nonEmptyPathValue <- Either.cond(
|
||||||
|
pathValue.nonEmpty,
|
||||||
|
pathValue,
|
||||||
|
"Provided DAR path is empty",
|
||||||
|
)
|
||||||
|
filename = Paths.get(nonEmptyPathValue).getFileName.toString
|
||||||
|
darData <- BinaryFileUtil.readByteStringFromFile(nonEmptyPathValue)
|
||||||
|
} yield ValidateDarRequest(
|
||||||
|
darData,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: PackageServiceStub,
|
||||||
|
request: ValidateDarRequest,
|
||||||
|
): Future[ValidateDarResponse] =
|
||||||
|
service.validateDar(request)
|
||||||
|
|
||||||
|
override def handleResponse(response: ValidateDarResponse): Either[String, String] =
|
||||||
|
response match {
|
||||||
|
case ValidateDarResponse(hash) => Right(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// file can be big. checking & vetting might take a while
|
||||||
|
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
final case class RemovePackage(
|
final case class RemovePackage(
|
||||||
packageId: String,
|
packageId: String,
|
||||||
force: Boolean,
|
force: Boolean,
|
||||||
|
@ -10,23 +10,611 @@ import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
|||||||
TimeoutType,
|
TimeoutType,
|
||||||
}
|
}
|
||||||
import com.digitalasset.canton.admin.api.client.data.*
|
import com.digitalasset.canton.admin.api.client.data.*
|
||||||
|
import com.digitalasset.canton.admin.api.client.data.topologyx.*
|
||||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||||
|
import com.digitalasset.canton.crypto.Fingerprint
|
||||||
import com.digitalasset.canton.topology.*
|
import com.digitalasset.canton.topology.*
|
||||||
|
import com.digitalasset.canton.topology.admin.grpc.BaseQueryX
|
||||||
import com.digitalasset.canton.topology.admin.v30
|
import com.digitalasset.canton.topology.admin.v30
|
||||||
|
import com.digitalasset.canton.topology.admin.v30.AuthorizeRequest.Type.{Proposal, TransactionHash}
|
||||||
|
import com.digitalasset.canton.topology.admin.v30.IdentityInitializationXServiceGrpc.IdentityInitializationXServiceStub
|
||||||
import com.digitalasset.canton.topology.admin.v30.TopologyAggregationServiceGrpc.TopologyAggregationServiceStub
|
import com.digitalasset.canton.topology.admin.v30.TopologyAggregationServiceGrpc.TopologyAggregationServiceStub
|
||||||
|
import com.digitalasset.canton.topology.admin.v30.TopologyManagerReadServiceGrpc.TopologyManagerReadServiceStub
|
||||||
|
import com.digitalasset.canton.topology.admin.v30.TopologyManagerWriteServiceGrpc.TopologyManagerWriteServiceStub
|
||||||
|
import com.digitalasset.canton.topology.admin.v30.{
|
||||||
|
AddTransactionsRequest,
|
||||||
|
AddTransactionsResponse,
|
||||||
|
AuthorizeRequest,
|
||||||
|
AuthorizeResponse,
|
||||||
|
ImportTopologySnapshotRequest,
|
||||||
|
ImportTopologySnapshotResponse,
|
||||||
|
ListTrafficStateRequest,
|
||||||
|
SignTransactionsRequest,
|
||||||
|
SignTransactionsResponse,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.GenericStoredTopologyTransactionsX
|
||||||
|
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.transaction.{
|
||||||
|
SignedTopologyTransactionX,
|
||||||
|
TopologyChangeOpX,
|
||||||
|
TopologyMappingX,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.version.ProtocolVersionValidation
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
import com.google.protobuf.timestamp.Timestamp
|
import com.google.protobuf.timestamp.Timestamp
|
||||||
import io.grpc.ManagedChannel
|
import io.grpc.ManagedChannel
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.Future
|
||||||
|
import scala.reflect.ClassTag
|
||||||
|
|
||||||
// TODO(#15161): Move commands to other file, e.g. related to VaultAdministration
|
|
||||||
object TopologyAdminCommands {
|
object TopologyAdminCommands {
|
||||||
|
|
||||||
|
object Read {
|
||||||
|
|
||||||
|
abstract class BaseCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
||||||
|
override type Svc = TopologyManagerReadServiceStub
|
||||||
|
override def createService(channel: ManagedChannel): TopologyManagerReadServiceStub =
|
||||||
|
v30.TopologyManagerReadServiceGrpc.stub(channel)
|
||||||
|
|
||||||
|
// command will potentially take a long time
|
||||||
|
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListTrafficControlState(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterMember: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListTrafficStateRequest,
|
||||||
|
v30.ListTrafficStateResponse,
|
||||||
|
Seq[ListTrafficStateResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListTrafficStateRequest] =
|
||||||
|
Right(
|
||||||
|
new ListTrafficStateRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterMember = filterMember,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListTrafficStateRequest,
|
||||||
|
): Future[v30.ListTrafficStateResponse] =
|
||||||
|
service.listTrafficState(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListTrafficStateResponse
|
||||||
|
): Either[String, Seq[ListTrafficStateResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListTrafficStateResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListNamespaceDelegation(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterNamespace: String,
|
||||||
|
filterTargetKey: Option[Fingerprint],
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListNamespaceDelegationRequest,
|
||||||
|
v30.ListNamespaceDelegationResponse,
|
||||||
|
Seq[ListNamespaceDelegationResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListNamespaceDelegationRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListNamespaceDelegationRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterNamespace = filterNamespace,
|
||||||
|
filterTargetKeyFingerprint = filterTargetKey.map(_.toProtoPrimitive).getOrElse(""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListNamespaceDelegationRequest,
|
||||||
|
): Future[v30.ListNamespaceDelegationResponse] =
|
||||||
|
service.listNamespaceDelegation(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListNamespaceDelegationResponse
|
||||||
|
): Either[String, Seq[ListNamespaceDelegationResult]] =
|
||||||
|
response.results.traverse(ListNamespaceDelegationResult.fromProtoV30).leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListDecentralizedNamespaceDefinition(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterNamespace: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListDecentralizedNamespaceDefinitionRequest,
|
||||||
|
v30.ListDecentralizedNamespaceDefinitionResponse,
|
||||||
|
Seq[ListDecentralizedNamespaceDefinitionResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest()
|
||||||
|
: Either[String, v30.ListDecentralizedNamespaceDefinitionRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListDecentralizedNamespaceDefinitionRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterNamespace = filterNamespace,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListDecentralizedNamespaceDefinitionRequest,
|
||||||
|
): Future[v30.ListDecentralizedNamespaceDefinitionResponse] =
|
||||||
|
service.listDecentralizedNamespaceDefinition(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListDecentralizedNamespaceDefinitionResponse
|
||||||
|
): Either[String, Seq[ListDecentralizedNamespaceDefinitionResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListDecentralizedNamespaceDefinitionResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListIdentifierDelegation(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterUid: String,
|
||||||
|
filterTargetKey: Option[Fingerprint],
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListIdentifierDelegationRequest,
|
||||||
|
v30.ListIdentifierDelegationResponse,
|
||||||
|
Seq[ListIdentifierDelegationResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListIdentifierDelegationRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListIdentifierDelegationRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterUid = filterUid,
|
||||||
|
filterTargetKeyFingerprint = filterTargetKey.map(_.toProtoPrimitive).getOrElse(""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListIdentifierDelegationRequest,
|
||||||
|
): Future[v30.ListIdentifierDelegationResponse] =
|
||||||
|
service.listIdentifierDelegation(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListIdentifierDelegationResponse
|
||||||
|
): Either[String, Seq[ListIdentifierDelegationResult]] =
|
||||||
|
response.results.traverse(ListIdentifierDelegationResult.fromProtoV30).leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListOwnerToKeyMapping(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterKeyOwnerType: Option[MemberCode],
|
||||||
|
filterKeyOwnerUid: String,
|
||||||
|
) extends BaseCommand[v30.ListOwnerToKeyMappingRequest, v30.ListOwnerToKeyMappingResponse, Seq[
|
||||||
|
ListOwnerToKeyMappingResult
|
||||||
|
]] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListOwnerToKeyMappingRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListOwnerToKeyMappingRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterKeyOwnerType = filterKeyOwnerType.map(_.toProtoPrimitive).getOrElse(""),
|
||||||
|
filterKeyOwnerUid = filterKeyOwnerUid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListOwnerToKeyMappingRequest,
|
||||||
|
): Future[v30.ListOwnerToKeyMappingResponse] =
|
||||||
|
service.listOwnerToKeyMapping(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListOwnerToKeyMappingResponse
|
||||||
|
): Either[String, Seq[ListOwnerToKeyMappingResult]] =
|
||||||
|
response.results.traverse(ListOwnerToKeyMappingResult.fromProtoV30).leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListDomainTrustCertificate(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterUid: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListDomainTrustCertificateRequest,
|
||||||
|
v30.ListDomainTrustCertificateResponse,
|
||||||
|
Seq[ListDomainTrustCertificateResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListDomainTrustCertificateRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListDomainTrustCertificateRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterUid = filterUid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListDomainTrustCertificateRequest,
|
||||||
|
): Future[v30.ListDomainTrustCertificateResponse] =
|
||||||
|
service.listDomainTrustCertificate(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListDomainTrustCertificateResponse
|
||||||
|
): Either[String, Seq[ListDomainTrustCertificateResult]] =
|
||||||
|
response.results.traverse(ListDomainTrustCertificateResult.fromProtoV30).leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListParticipantDomainPermission(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterUid: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListParticipantDomainPermissionRequest,
|
||||||
|
v30.ListParticipantDomainPermissionResponse,
|
||||||
|
Seq[ListParticipantDomainPermissionResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListParticipantDomainPermissionRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListParticipantDomainPermissionRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterUid = filterUid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListParticipantDomainPermissionRequest,
|
||||||
|
): Future[v30.ListParticipantDomainPermissionResponse] =
|
||||||
|
service.listParticipantDomainPermission(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListParticipantDomainPermissionResponse
|
||||||
|
): Either[String, Seq[ListParticipantDomainPermissionResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListParticipantDomainPermissionResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListPartyHostingLimits(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterUid: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListPartyHostingLimitsRequest,
|
||||||
|
v30.ListPartyHostingLimitsResponse,
|
||||||
|
Seq[ListPartyHostingLimitsResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListPartyHostingLimitsRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListPartyHostingLimitsRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterUid = filterUid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListPartyHostingLimitsRequest,
|
||||||
|
): Future[v30.ListPartyHostingLimitsResponse] =
|
||||||
|
service.listPartyHostingLimits(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListPartyHostingLimitsResponse
|
||||||
|
): Either[String, Seq[ListPartyHostingLimitsResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListPartyHostingLimitsResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListVettedPackages(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterParticipant: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListVettedPackagesRequest,
|
||||||
|
v30.ListVettedPackagesResponse,
|
||||||
|
Seq[ListVettedPackagesResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListVettedPackagesRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListVettedPackagesRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterParticipant = filterParticipant,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListVettedPackagesRequest,
|
||||||
|
): Future[v30.ListVettedPackagesResponse] =
|
||||||
|
service.listVettedPackages(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListVettedPackagesResponse
|
||||||
|
): Either[String, Seq[ListVettedPackagesResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListVettedPackagesResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListPartyToParticipant(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterParty: String,
|
||||||
|
filterParticipant: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListPartyToParticipantRequest,
|
||||||
|
v30.ListPartyToParticipantResponse,
|
||||||
|
Seq[ListPartyToParticipantResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListPartyToParticipantRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListPartyToParticipantRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterParty = filterParty,
|
||||||
|
filterParticipant = filterParticipant,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListPartyToParticipantRequest,
|
||||||
|
): Future[v30.ListPartyToParticipantResponse] =
|
||||||
|
service.listPartyToParticipant(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListPartyToParticipantResponse
|
||||||
|
): Either[String, Seq[ListPartyToParticipantResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListPartyToParticipantResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListAuthorityOf(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterParty: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListAuthorityOfRequest,
|
||||||
|
v30.ListAuthorityOfResponse,
|
||||||
|
Seq[ListAuthorityOfResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListAuthorityOfRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListAuthorityOfRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterParty = filterParty,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListAuthorityOfRequest,
|
||||||
|
): Future[v30.ListAuthorityOfResponse] =
|
||||||
|
service.listAuthorityOf(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListAuthorityOfResponse
|
||||||
|
): Either[String, Seq[ListAuthorityOfResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListAuthorityOfResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class DomainParametersState(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterDomain: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListDomainParametersStateRequest,
|
||||||
|
v30.ListDomainParametersStateResponse,
|
||||||
|
Seq[ListDomainParametersStateResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListDomainParametersStateRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListDomainParametersStateRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterDomain = filterDomain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListDomainParametersStateRequest,
|
||||||
|
): Future[v30.ListDomainParametersStateResponse] =
|
||||||
|
service.listDomainParametersState(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListDomainParametersStateResponse
|
||||||
|
): Either[String, Seq[ListDomainParametersStateResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListDomainParametersStateResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class MediatorDomainState(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterDomain: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListMediatorDomainStateRequest,
|
||||||
|
v30.ListMediatorDomainStateResponse,
|
||||||
|
Seq[ListMediatorDomainStateResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListMediatorDomainStateRequest] =
|
||||||
|
Right(
|
||||||
|
v30.ListMediatorDomainStateRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterDomain = filterDomain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListMediatorDomainStateRequest,
|
||||||
|
): Future[v30.ListMediatorDomainStateResponse] =
|
||||||
|
service.listMediatorDomainState(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListMediatorDomainStateResponse
|
||||||
|
): Either[String, Seq[ListMediatorDomainStateResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListMediatorDomainStateResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class SequencerDomainState(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterDomain: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListSequencerDomainStateRequest,
|
||||||
|
v30.ListSequencerDomainStateResponse,
|
||||||
|
Seq[ListSequencerDomainStateResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListSequencerDomainStateRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListSequencerDomainStateRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterDomain = filterDomain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListSequencerDomainStateRequest,
|
||||||
|
): Future[v30.ListSequencerDomainStateResponse] =
|
||||||
|
service.listSequencerDomainState(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListSequencerDomainStateResponse
|
||||||
|
): Either[String, Seq[ListSequencerDomainStateResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListSequencerDomainStateResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class PurgeTopologyTransaction(
|
||||||
|
query: BaseQueryX,
|
||||||
|
filterDomain: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListPurgeTopologyTransactionRequest,
|
||||||
|
v30.ListPurgeTopologyTransactionResponse,
|
||||||
|
Seq[ListPurgeTopologyTransactionResult],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListPurgeTopologyTransactionRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListPurgeTopologyTransactionRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
filterDomain = filterDomain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListPurgeTopologyTransactionRequest,
|
||||||
|
): Future[v30.ListPurgeTopologyTransactionResponse] =
|
||||||
|
service.listPurgeTopologyTransaction(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListPurgeTopologyTransactionResponse
|
||||||
|
): Either[String, Seq[ListPurgeTopologyTransactionResult]] =
|
||||||
|
response.results
|
||||||
|
.traverse(ListPurgeTopologyTransactionResult.fromProtoV30)
|
||||||
|
.leftMap(_.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListStores()
|
||||||
|
extends BaseCommand[v30.ListAvailableStoresRequest, v30.ListAvailableStoresResponse, Seq[
|
||||||
|
String
|
||||||
|
]] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.ListAvailableStoresRequest] =
|
||||||
|
Right(v30.ListAvailableStoresRequest())
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListAvailableStoresRequest,
|
||||||
|
): Future[v30.ListAvailableStoresResponse] =
|
||||||
|
service.listAvailableStores(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListAvailableStoresResponse
|
||||||
|
): Either[String, Seq[String]] =
|
||||||
|
Right(response.storeIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ListAll(
|
||||||
|
query: BaseQueryX,
|
||||||
|
excludeMappings: Seq[String],
|
||||||
|
filterNamespace: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ListAllRequest,
|
||||||
|
v30.ListAllResponse,
|
||||||
|
GenericStoredTopologyTransactionsX,
|
||||||
|
] {
|
||||||
|
override def createRequest(): Either[String, v30.ListAllRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ListAllRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
excludeMappings = excludeMappings,
|
||||||
|
filterNamespace = filterNamespace,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ListAllRequest,
|
||||||
|
): Future[v30.ListAllResponse] = service.listAll(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ListAllResponse
|
||||||
|
): Either[String, GenericStoredTopologyTransactionsX] =
|
||||||
|
response.result
|
||||||
|
.fold[Either[String, GenericStoredTopologyTransactionsX]](
|
||||||
|
Right(StoredTopologyTransactionsX.empty)
|
||||||
|
) { collection =>
|
||||||
|
StoredTopologyTransactionsX.fromProtoV30(collection).leftMap(_.toString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final case class ExportTopologySnapshot(
|
||||||
|
query: BaseQueryX,
|
||||||
|
excludeMappings: Seq[String],
|
||||||
|
filterNamespace: String,
|
||||||
|
) extends BaseCommand[
|
||||||
|
v30.ExportTopologySnapshotRequest,
|
||||||
|
v30.ExportTopologySnapshotResponse,
|
||||||
|
ByteString,
|
||||||
|
] {
|
||||||
|
override def createRequest(): Either[String, v30.ExportTopologySnapshotRequest] =
|
||||||
|
Right(
|
||||||
|
new v30.ExportTopologySnapshotRequest(
|
||||||
|
baseQuery = Some(query.toProtoV1),
|
||||||
|
excludeMappings = excludeMappings,
|
||||||
|
filterNamespace = filterNamespace,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerReadServiceStub,
|
||||||
|
request: v30.ExportTopologySnapshotRequest,
|
||||||
|
): Future[v30.ExportTopologySnapshotResponse] = service.exportTopologySnapshot(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.ExportTopologySnapshotResponse
|
||||||
|
): Either[String, ByteString] =
|
||||||
|
Right(response.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object Aggregation {
|
object Aggregation {
|
||||||
|
|
||||||
abstract class BaseCommand[Req, Res, Result] extends GrpcAdminCommand[Req, Res, Result] {
|
abstract class BaseCommand[Req, Res, Result] extends GrpcAdminCommand[Req, Res, Result] {
|
||||||
override type Svc = TopologyAggregationServiceStub
|
override type Svc = TopologyAggregationServiceStub
|
||||||
|
|
||||||
override def createService(channel: ManagedChannel): TopologyAggregationServiceStub =
|
override def createService(channel: ManagedChannel): TopologyAggregationServiceStub =
|
||||||
v30.TopologyAggregationServiceGrpc.stub(channel)
|
v30.TopologyAggregationServiceGrpc.stub(channel)
|
||||||
}
|
}
|
||||||
@ -105,4 +693,239 @@ object TopologyAdminCommands {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Write {
|
||||||
|
abstract class BaseWriteCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
||||||
|
override type Svc = TopologyManagerWriteServiceStub
|
||||||
|
|
||||||
|
override def createService(channel: ManagedChannel): TopologyManagerWriteServiceStub =
|
||||||
|
v30.TopologyManagerWriteServiceGrpc.stub(channel)
|
||||||
|
|
||||||
|
// command will potentially take a long time
|
||||||
|
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class AddTransactions(
|
||||||
|
transactions: Seq[GenericSignedTopologyTransactionX],
|
||||||
|
store: String,
|
||||||
|
) extends BaseWriteCommand[AddTransactionsRequest, AddTransactionsResponse, Unit] {
|
||||||
|
override def createRequest(): Either[String, AddTransactionsRequest] = {
|
||||||
|
Right(AddTransactionsRequest(transactions.map(_.toProtoV30), forceChange = false, store))
|
||||||
|
}
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerWriteServiceStub,
|
||||||
|
request: AddTransactionsRequest,
|
||||||
|
): Future[AddTransactionsResponse] = service.addTransactions(request)
|
||||||
|
override def handleResponse(response: AddTransactionsResponse): Either[String, Unit] =
|
||||||
|
Right(())
|
||||||
|
}
|
||||||
|
final case class ImportTopologySnapshot(
|
||||||
|
topologySnapshot: ByteString,
|
||||||
|
store: String,
|
||||||
|
) extends BaseWriteCommand[
|
||||||
|
ImportTopologySnapshotRequest,
|
||||||
|
ImportTopologySnapshotResponse,
|
||||||
|
Unit,
|
||||||
|
] {
|
||||||
|
override def createRequest(): Either[String, ImportTopologySnapshotRequest] = {
|
||||||
|
Right(
|
||||||
|
ImportTopologySnapshotRequest(
|
||||||
|
topologySnapshot,
|
||||||
|
forceChange = false,
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerWriteServiceStub,
|
||||||
|
request: ImportTopologySnapshotRequest,
|
||||||
|
): Future[ImportTopologySnapshotResponse] = service.importTopologySnapshot(request)
|
||||||
|
override def handleResponse(
|
||||||
|
response: ImportTopologySnapshotResponse
|
||||||
|
): Either[String, Unit] = Right(())
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class SignTransactions(
|
||||||
|
transactions: Seq[GenericSignedTopologyTransactionX],
|
||||||
|
signedBy: Seq[Fingerprint],
|
||||||
|
) extends BaseWriteCommand[SignTransactionsRequest, SignTransactionsResponse, Seq[
|
||||||
|
GenericSignedTopologyTransactionX
|
||||||
|
]] {
|
||||||
|
override def createRequest(): Either[String, SignTransactionsRequest] = {
|
||||||
|
Right(
|
||||||
|
SignTransactionsRequest(transactions.map(_.toProtoV30), signedBy.map(_.toProtoPrimitive))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerWriteServiceStub,
|
||||||
|
request: SignTransactionsRequest,
|
||||||
|
): Future[SignTransactionsResponse] = service.signTransactions(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: SignTransactionsResponse
|
||||||
|
): Either[String, Seq[GenericSignedTopologyTransactionX]] =
|
||||||
|
response.transactions
|
||||||
|
.traverse(tx =>
|
||||||
|
SignedTopologyTransactionX.fromProtoV30(ProtocolVersionValidation.NoValidation, tx)
|
||||||
|
)
|
||||||
|
.leftMap(_.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class Propose[M <: TopologyMappingX: ClassTag](
|
||||||
|
mapping: Either[String, M],
|
||||||
|
signedBy: Seq[Fingerprint],
|
||||||
|
change: TopologyChangeOpX,
|
||||||
|
serial: Option[PositiveInt],
|
||||||
|
mustFullyAuthorize: Boolean,
|
||||||
|
forceChange: Boolean,
|
||||||
|
store: String,
|
||||||
|
) extends BaseWriteCommand[
|
||||||
|
AuthorizeRequest,
|
||||||
|
AuthorizeResponse,
|
||||||
|
SignedTopologyTransactionX[TopologyChangeOpX, M],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, AuthorizeRequest] = mapping.map(m =>
|
||||||
|
AuthorizeRequest(
|
||||||
|
Proposal(
|
||||||
|
AuthorizeRequest.Proposal(
|
||||||
|
change.toProto,
|
||||||
|
serial.map(_.value).getOrElse(0),
|
||||||
|
Some(m.toProtoV30),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
mustFullyAuthorize = mustFullyAuthorize,
|
||||||
|
forceChange = forceChange,
|
||||||
|
signedBy = signedBy.map(_.toProtoPrimitive),
|
||||||
|
store,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerWriteServiceStub,
|
||||||
|
request: AuthorizeRequest,
|
||||||
|
): Future[AuthorizeResponse] = service.authorize(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: AuthorizeResponse
|
||||||
|
): Either[String, SignedTopologyTransactionX[TopologyChangeOpX, M]] = response.transaction
|
||||||
|
.toRight("no transaction in response")
|
||||||
|
.flatMap(
|
||||||
|
SignedTopologyTransactionX
|
||||||
|
.fromProtoV30(ProtocolVersionValidation.NoValidation, _)
|
||||||
|
.leftMap(_.message)
|
||||||
|
.flatMap(tx =>
|
||||||
|
tx.selectMapping[M]
|
||||||
|
.toRight(
|
||||||
|
s"Expected mapping ${ClassTag[M].getClass.getSimpleName}, but received: ${tx.mapping.getClass.getSimpleName}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
object Propose {
|
||||||
|
def apply[M <: TopologyMappingX: ClassTag](
|
||||||
|
mapping: M,
|
||||||
|
signedBy: Seq[Fingerprint],
|
||||||
|
store: String,
|
||||||
|
serial: Option[PositiveInt] = None,
|
||||||
|
change: TopologyChangeOpX = TopologyChangeOpX.Replace,
|
||||||
|
mustFullyAuthorize: Boolean = false,
|
||||||
|
forceChange: Boolean = false,
|
||||||
|
): Propose[M] =
|
||||||
|
Propose(Right(mapping), signedBy, change, serial, mustFullyAuthorize, forceChange, store)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class Authorize[M <: TopologyMappingX: ClassTag](
|
||||||
|
transactionHash: String,
|
||||||
|
mustFullyAuthorize: Boolean,
|
||||||
|
signedBy: Seq[Fingerprint],
|
||||||
|
store: String,
|
||||||
|
) extends BaseWriteCommand[
|
||||||
|
AuthorizeRequest,
|
||||||
|
AuthorizeResponse,
|
||||||
|
SignedTopologyTransactionX[TopologyChangeOpX, M],
|
||||||
|
] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, AuthorizeRequest] = Right(
|
||||||
|
AuthorizeRequest(
|
||||||
|
TransactionHash(transactionHash),
|
||||||
|
mustFullyAuthorize = mustFullyAuthorize,
|
||||||
|
forceChange = false,
|
||||||
|
signedBy = signedBy.map(_.toProtoPrimitive),
|
||||||
|
store = store,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: TopologyManagerWriteServiceStub,
|
||||||
|
request: AuthorizeRequest,
|
||||||
|
): Future[AuthorizeResponse] = service.authorize(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: AuthorizeResponse
|
||||||
|
): Either[String, SignedTopologyTransactionX[TopologyChangeOpX, M]] = response.transaction
|
||||||
|
.toRight("no transaction in response")
|
||||||
|
.flatMap(
|
||||||
|
SignedTopologyTransactionX
|
||||||
|
.fromProtoV30(ProtocolVersionValidation.NoValidation, _)
|
||||||
|
.leftMap(_.message)
|
||||||
|
.flatMap(tx =>
|
||||||
|
tx.selectMapping[M]
|
||||||
|
.toRight(
|
||||||
|
s"Expected mapping ${ClassTag[M].getClass.getSimpleName}, but received: ${tx.mapping.getClass.getSimpleName}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Init {
|
||||||
|
|
||||||
|
abstract class BaseInitializationService[Req, Resp, Res]
|
||||||
|
extends GrpcAdminCommand[Req, Resp, Res] {
|
||||||
|
override type Svc = IdentityInitializationXServiceStub
|
||||||
|
override def createService(channel: ManagedChannel): IdentityInitializationXServiceStub =
|
||||||
|
v30.IdentityInitializationXServiceGrpc.stub(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class InitId(identifier: String)
|
||||||
|
extends BaseInitializationService[v30.InitIdRequest, v30.InitIdResponse, Unit] {
|
||||||
|
|
||||||
|
override def createRequest(): Either[String, v30.InitIdRequest] =
|
||||||
|
Right(v30.InitIdRequest(identifier))
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: IdentityInitializationXServiceStub,
|
||||||
|
request: v30.InitIdRequest,
|
||||||
|
): Future[v30.InitIdResponse] =
|
||||||
|
service.initId(request)
|
||||||
|
|
||||||
|
override def handleResponse(response: v30.InitIdResponse): Either[String, Unit] =
|
||||||
|
Right(())
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class GetId()
|
||||||
|
extends BaseInitializationService[v30.GetIdRequest, v30.GetIdResponse, UniqueIdentifier] {
|
||||||
|
override def createRequest(): Either[String, v30.GetIdRequest] =
|
||||||
|
Right(v30.GetIdRequest())
|
||||||
|
|
||||||
|
override def submitRequest(
|
||||||
|
service: IdentityInitializationXServiceStub,
|
||||||
|
request: v30.GetIdRequest,
|
||||||
|
): Future[v30.GetIdResponse] =
|
||||||
|
service.getId(request)
|
||||||
|
|
||||||
|
override def handleResponse(
|
||||||
|
response: v30.GetIdResponse
|
||||||
|
): Either[String, UniqueIdentifier] = {
|
||||||
|
if (response.uniqueIdentifier.nonEmpty)
|
||||||
|
UniqueIdentifier.fromProtoPrimitive_(response.uniqueIdentifier)
|
||||||
|
else
|
||||||
|
Left(
|
||||||
|
s"Node is not initialized and therefore does not have an Id assigned yet."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,843 +0,0 @@
|
|||||||
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package com.digitalasset.canton.admin.api.client.commands
|
|
||||||
|
|
||||||
import cats.syntax.either.*
|
|
||||||
import cats.syntax.traverse.*
|
|
||||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
|
||||||
DefaultUnboundedTimeout,
|
|
||||||
TimeoutType,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.admin.api.client.data.topologyx.*
|
|
||||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
|
||||||
import com.digitalasset.canton.crypto.Fingerprint
|
|
||||||
import com.digitalasset.canton.topology.*
|
|
||||||
import com.digitalasset.canton.topology.admin.grpc.BaseQueryX
|
|
||||||
import com.digitalasset.canton.topology.admin.v30
|
|
||||||
import com.digitalasset.canton.topology.admin.v30.AuthorizeRequest.Type.{Proposal, TransactionHash}
|
|
||||||
import com.digitalasset.canton.topology.admin.v30.IdentityInitializationXServiceGrpc.IdentityInitializationXServiceStub
|
|
||||||
import com.digitalasset.canton.topology.admin.v30.TopologyManagerReadServiceGrpc.TopologyManagerReadServiceStub
|
|
||||||
import com.digitalasset.canton.topology.admin.v30.TopologyManagerWriteServiceGrpc.TopologyManagerWriteServiceStub
|
|
||||||
import com.digitalasset.canton.topology.admin.v30.{
|
|
||||||
AddTransactionsRequest,
|
|
||||||
AddTransactionsResponse,
|
|
||||||
AuthorizeRequest,
|
|
||||||
AuthorizeResponse,
|
|
||||||
ImportTopologySnapshotRequest,
|
|
||||||
ImportTopologySnapshotResponse,
|
|
||||||
ListTrafficStateRequest,
|
|
||||||
SignTransactionsRequest,
|
|
||||||
SignTransactionsResponse,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.GenericStoredTopologyTransactionsX
|
|
||||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.transaction.{
|
|
||||||
SignedTopologyTransactionX,
|
|
||||||
TopologyChangeOpX,
|
|
||||||
TopologyMappingX,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.version.ProtocolVersionValidation
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import io.grpc.ManagedChannel
|
|
||||||
|
|
||||||
import scala.concurrent.Future
|
|
||||||
import scala.reflect.ClassTag
|
|
||||||
|
|
||||||
object TopologyAdminCommandsX {
|
|
||||||
|
|
||||||
object Read {
|
|
||||||
|
|
||||||
abstract class BaseCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
|
||||||
override type Svc = TopologyManagerReadServiceStub
|
|
||||||
override def createService(channel: ManagedChannel): TopologyManagerReadServiceStub =
|
|
||||||
v30.TopologyManagerReadServiceGrpc.stub(channel)
|
|
||||||
|
|
||||||
// command will potentially take a long time
|
|
||||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListTrafficControlState(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterMember: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListTrafficStateRequest,
|
|
||||||
v30.ListTrafficStateResponse,
|
|
||||||
Seq[ListTrafficStateResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListTrafficStateRequest] =
|
|
||||||
Right(
|
|
||||||
new ListTrafficStateRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterMember = filterMember,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListTrafficStateRequest,
|
|
||||||
): Future[v30.ListTrafficStateResponse] =
|
|
||||||
service.listTrafficState(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListTrafficStateResponse
|
|
||||||
): Either[String, Seq[ListTrafficStateResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListTrafficStateResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListNamespaceDelegation(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterNamespace: String,
|
|
||||||
filterTargetKey: Option[Fingerprint],
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListNamespaceDelegationRequest,
|
|
||||||
v30.ListNamespaceDelegationResponse,
|
|
||||||
Seq[ListNamespaceDelegationResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListNamespaceDelegationRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListNamespaceDelegationRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterNamespace = filterNamespace,
|
|
||||||
filterTargetKeyFingerprint = filterTargetKey.map(_.toProtoPrimitive).getOrElse(""),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListNamespaceDelegationRequest,
|
|
||||||
): Future[v30.ListNamespaceDelegationResponse] =
|
|
||||||
service.listNamespaceDelegation(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListNamespaceDelegationResponse
|
|
||||||
): Either[String, Seq[ListNamespaceDelegationResult]] =
|
|
||||||
response.results.traverse(ListNamespaceDelegationResult.fromProtoV30).leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListDecentralizedNamespaceDefinition(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterNamespace: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListDecentralizedNamespaceDefinitionRequest,
|
|
||||||
v30.ListDecentralizedNamespaceDefinitionResponse,
|
|
||||||
Seq[ListDecentralizedNamespaceDefinitionResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest()
|
|
||||||
: Either[String, v30.ListDecentralizedNamespaceDefinitionRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListDecentralizedNamespaceDefinitionRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterNamespace = filterNamespace,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListDecentralizedNamespaceDefinitionRequest,
|
|
||||||
): Future[v30.ListDecentralizedNamespaceDefinitionResponse] =
|
|
||||||
service.listDecentralizedNamespaceDefinition(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListDecentralizedNamespaceDefinitionResponse
|
|
||||||
): Either[String, Seq[ListDecentralizedNamespaceDefinitionResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListDecentralizedNamespaceDefinitionResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListIdentifierDelegation(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterUid: String,
|
|
||||||
filterTargetKey: Option[Fingerprint],
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListIdentifierDelegationRequest,
|
|
||||||
v30.ListIdentifierDelegationResponse,
|
|
||||||
Seq[ListIdentifierDelegationResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListIdentifierDelegationRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListIdentifierDelegationRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterUid = filterUid,
|
|
||||||
filterTargetKeyFingerprint = filterTargetKey.map(_.toProtoPrimitive).getOrElse(""),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListIdentifierDelegationRequest,
|
|
||||||
): Future[v30.ListIdentifierDelegationResponse] =
|
|
||||||
service.listIdentifierDelegation(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListIdentifierDelegationResponse
|
|
||||||
): Either[String, Seq[ListIdentifierDelegationResult]] =
|
|
||||||
response.results.traverse(ListIdentifierDelegationResult.fromProtoV30).leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListOwnerToKeyMapping(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterKeyOwnerType: Option[MemberCode],
|
|
||||||
filterKeyOwnerUid: String,
|
|
||||||
) extends BaseCommand[v30.ListOwnerToKeyMappingRequest, v30.ListOwnerToKeyMappingResponse, Seq[
|
|
||||||
ListOwnerToKeyMappingResult
|
|
||||||
]] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListOwnerToKeyMappingRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListOwnerToKeyMappingRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterKeyOwnerType = filterKeyOwnerType.map(_.toProtoPrimitive).getOrElse(""),
|
|
||||||
filterKeyOwnerUid = filterKeyOwnerUid,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListOwnerToKeyMappingRequest,
|
|
||||||
): Future[v30.ListOwnerToKeyMappingResponse] =
|
|
||||||
service.listOwnerToKeyMapping(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListOwnerToKeyMappingResponse
|
|
||||||
): Either[String, Seq[ListOwnerToKeyMappingResult]] =
|
|
||||||
response.results.traverse(ListOwnerToKeyMappingResult.fromProtoV30).leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListDomainTrustCertificate(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterUid: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListDomainTrustCertificateRequest,
|
|
||||||
v30.ListDomainTrustCertificateResponse,
|
|
||||||
Seq[ListDomainTrustCertificateResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListDomainTrustCertificateRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListDomainTrustCertificateRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterUid = filterUid,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListDomainTrustCertificateRequest,
|
|
||||||
): Future[v30.ListDomainTrustCertificateResponse] =
|
|
||||||
service.listDomainTrustCertificate(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListDomainTrustCertificateResponse
|
|
||||||
): Either[String, Seq[ListDomainTrustCertificateResult]] =
|
|
||||||
response.results.traverse(ListDomainTrustCertificateResult.fromProtoV30).leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListParticipantDomainPermission(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterUid: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListParticipantDomainPermissionRequest,
|
|
||||||
v30.ListParticipantDomainPermissionResponse,
|
|
||||||
Seq[ListParticipantDomainPermissionResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListParticipantDomainPermissionRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListParticipantDomainPermissionRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterUid = filterUid,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListParticipantDomainPermissionRequest,
|
|
||||||
): Future[v30.ListParticipantDomainPermissionResponse] =
|
|
||||||
service.listParticipantDomainPermission(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListParticipantDomainPermissionResponse
|
|
||||||
): Either[String, Seq[ListParticipantDomainPermissionResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListParticipantDomainPermissionResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListPartyHostingLimits(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterUid: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListPartyHostingLimitsRequest,
|
|
||||||
v30.ListPartyHostingLimitsResponse,
|
|
||||||
Seq[ListPartyHostingLimitsResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListPartyHostingLimitsRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListPartyHostingLimitsRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterUid = filterUid,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListPartyHostingLimitsRequest,
|
|
||||||
): Future[v30.ListPartyHostingLimitsResponse] =
|
|
||||||
service.listPartyHostingLimits(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListPartyHostingLimitsResponse
|
|
||||||
): Either[String, Seq[ListPartyHostingLimitsResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListPartyHostingLimitsResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListVettedPackages(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterParticipant: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListVettedPackagesRequest,
|
|
||||||
v30.ListVettedPackagesResponse,
|
|
||||||
Seq[ListVettedPackagesResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListVettedPackagesRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListVettedPackagesRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterParticipant = filterParticipant,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListVettedPackagesRequest,
|
|
||||||
): Future[v30.ListVettedPackagesResponse] =
|
|
||||||
service.listVettedPackages(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListVettedPackagesResponse
|
|
||||||
): Either[String, Seq[ListVettedPackagesResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListVettedPackagesResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListPartyToParticipant(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterParty: String,
|
|
||||||
filterParticipant: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListPartyToParticipantRequest,
|
|
||||||
v30.ListPartyToParticipantResponse,
|
|
||||||
Seq[ListPartyToParticipantResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListPartyToParticipantRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListPartyToParticipantRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterParty = filterParty,
|
|
||||||
filterParticipant = filterParticipant,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListPartyToParticipantRequest,
|
|
||||||
): Future[v30.ListPartyToParticipantResponse] =
|
|
||||||
service.listPartyToParticipant(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListPartyToParticipantResponse
|
|
||||||
): Either[String, Seq[ListPartyToParticipantResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListPartyToParticipantResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListAuthorityOf(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterParty: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListAuthorityOfRequest,
|
|
||||||
v30.ListAuthorityOfResponse,
|
|
||||||
Seq[ListAuthorityOfResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListAuthorityOfRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListAuthorityOfRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterParty = filterParty,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListAuthorityOfRequest,
|
|
||||||
): Future[v30.ListAuthorityOfResponse] =
|
|
||||||
service.listAuthorityOf(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListAuthorityOfResponse
|
|
||||||
): Either[String, Seq[ListAuthorityOfResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListAuthorityOfResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class DomainParametersState(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterDomain: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListDomainParametersStateRequest,
|
|
||||||
v30.ListDomainParametersStateResponse,
|
|
||||||
Seq[ListDomainParametersStateResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListDomainParametersStateRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListDomainParametersStateRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterDomain = filterDomain,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListDomainParametersStateRequest,
|
|
||||||
): Future[v30.ListDomainParametersStateResponse] =
|
|
||||||
service.listDomainParametersState(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListDomainParametersStateResponse
|
|
||||||
): Either[String, Seq[ListDomainParametersStateResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListDomainParametersStateResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class MediatorDomainState(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterDomain: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListMediatorDomainStateRequest,
|
|
||||||
v30.ListMediatorDomainStateResponse,
|
|
||||||
Seq[ListMediatorDomainStateResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListMediatorDomainStateRequest] =
|
|
||||||
Right(
|
|
||||||
v30.ListMediatorDomainStateRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterDomain = filterDomain,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListMediatorDomainStateRequest,
|
|
||||||
): Future[v30.ListMediatorDomainStateResponse] =
|
|
||||||
service.listMediatorDomainState(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListMediatorDomainStateResponse
|
|
||||||
): Either[String, Seq[ListMediatorDomainStateResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListMediatorDomainStateResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class SequencerDomainState(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterDomain: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListSequencerDomainStateRequest,
|
|
||||||
v30.ListSequencerDomainStateResponse,
|
|
||||||
Seq[ListSequencerDomainStateResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListSequencerDomainStateRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListSequencerDomainStateRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterDomain = filterDomain,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListSequencerDomainStateRequest,
|
|
||||||
): Future[v30.ListSequencerDomainStateResponse] =
|
|
||||||
service.listSequencerDomainState(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListSequencerDomainStateResponse
|
|
||||||
): Either[String, Seq[ListSequencerDomainStateResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListSequencerDomainStateResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class PurgeTopologyTransaction(
|
|
||||||
query: BaseQueryX,
|
|
||||||
filterDomain: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListPurgeTopologyTransactionRequest,
|
|
||||||
v30.ListPurgeTopologyTransactionResponse,
|
|
||||||
Seq[ListPurgeTopologyTransactionResult],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListPurgeTopologyTransactionRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListPurgeTopologyTransactionRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
filterDomain = filterDomain,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListPurgeTopologyTransactionRequest,
|
|
||||||
): Future[v30.ListPurgeTopologyTransactionResponse] =
|
|
||||||
service.listPurgeTopologyTransaction(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListPurgeTopologyTransactionResponse
|
|
||||||
): Either[String, Seq[ListPurgeTopologyTransactionResult]] =
|
|
||||||
response.results
|
|
||||||
.traverse(ListPurgeTopologyTransactionResult.fromProtoV30)
|
|
||||||
.leftMap(_.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListStores()
|
|
||||||
extends BaseCommand[v30.ListAvailableStoresRequest, v30.ListAvailableStoresResponse, Seq[
|
|
||||||
String
|
|
||||||
]] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.ListAvailableStoresRequest] =
|
|
||||||
Right(v30.ListAvailableStoresRequest())
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListAvailableStoresRequest,
|
|
||||||
): Future[v30.ListAvailableStoresResponse] =
|
|
||||||
service.listAvailableStores(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListAvailableStoresResponse
|
|
||||||
): Either[String, Seq[String]] =
|
|
||||||
Right(response.storeIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ListAll(
|
|
||||||
query: BaseQueryX,
|
|
||||||
excludeMappings: Seq[String],
|
|
||||||
filterNamespace: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ListAllRequest,
|
|
||||||
v30.ListAllResponse,
|
|
||||||
GenericStoredTopologyTransactionsX,
|
|
||||||
] {
|
|
||||||
override def createRequest(): Either[String, v30.ListAllRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ListAllRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
excludeMappings = excludeMappings,
|
|
||||||
filterNamespace = filterNamespace,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ListAllRequest,
|
|
||||||
): Future[v30.ListAllResponse] = service.listAll(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ListAllResponse
|
|
||||||
): Either[String, GenericStoredTopologyTransactionsX] =
|
|
||||||
response.result
|
|
||||||
.fold[Either[String, GenericStoredTopologyTransactionsX]](
|
|
||||||
Right(StoredTopologyTransactionsX.empty)
|
|
||||||
) { collection =>
|
|
||||||
StoredTopologyTransactionsX.fromProtoV30(collection).leftMap(_.toString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final case class ExportTopologySnapshot(
|
|
||||||
query: BaseQueryX,
|
|
||||||
excludeMappings: Seq[String],
|
|
||||||
filterNamespace: String,
|
|
||||||
) extends BaseCommand[
|
|
||||||
v30.ExportTopologySnapshotRequest,
|
|
||||||
v30.ExportTopologySnapshotResponse,
|
|
||||||
ByteString,
|
|
||||||
] {
|
|
||||||
override def createRequest(): Either[String, v30.ExportTopologySnapshotRequest] =
|
|
||||||
Right(
|
|
||||||
new v30.ExportTopologySnapshotRequest(
|
|
||||||
baseQuery = Some(query.toProtoV1),
|
|
||||||
excludeMappings = excludeMappings,
|
|
||||||
filterNamespace = filterNamespace,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerReadServiceStub,
|
|
||||||
request: v30.ExportTopologySnapshotRequest,
|
|
||||||
): Future[v30.ExportTopologySnapshotResponse] = service.exportTopologySnapshot(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.ExportTopologySnapshotResponse
|
|
||||||
): Either[String, ByteString] =
|
|
||||||
Right(response.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Write {
|
|
||||||
abstract class BaseWriteCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
|
||||||
override type Svc = TopologyManagerWriteServiceStub
|
|
||||||
|
|
||||||
override def createService(channel: ManagedChannel): TopologyManagerWriteServiceStub =
|
|
||||||
v30.TopologyManagerWriteServiceGrpc.stub(channel)
|
|
||||||
|
|
||||||
// command will potentially take a long time
|
|
||||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class AddTransactions(
|
|
||||||
transactions: Seq[GenericSignedTopologyTransactionX],
|
|
||||||
store: String,
|
|
||||||
) extends BaseWriteCommand[AddTransactionsRequest, AddTransactionsResponse, Unit] {
|
|
||||||
override def createRequest(): Either[String, AddTransactionsRequest] = {
|
|
||||||
Right(AddTransactionsRequest(transactions.map(_.toProtoV30), forceChange = false, store))
|
|
||||||
}
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerWriteServiceStub,
|
|
||||||
request: AddTransactionsRequest,
|
|
||||||
): Future[AddTransactionsResponse] = service.addTransactions(request)
|
|
||||||
override def handleResponse(response: AddTransactionsResponse): Either[String, Unit] =
|
|
||||||
Right(())
|
|
||||||
}
|
|
||||||
final case class ImportTopologySnapshot(
|
|
||||||
topologySnapshot: ByteString,
|
|
||||||
store: String,
|
|
||||||
) extends BaseWriteCommand[
|
|
||||||
ImportTopologySnapshotRequest,
|
|
||||||
ImportTopologySnapshotResponse,
|
|
||||||
Unit,
|
|
||||||
] {
|
|
||||||
override def createRequest(): Either[String, ImportTopologySnapshotRequest] = {
|
|
||||||
Right(
|
|
||||||
ImportTopologySnapshotRequest(
|
|
||||||
topologySnapshot,
|
|
||||||
forceChange = false,
|
|
||||||
store,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerWriteServiceStub,
|
|
||||||
request: ImportTopologySnapshotRequest,
|
|
||||||
): Future[ImportTopologySnapshotResponse] = service.importTopologySnapshot(request)
|
|
||||||
override def handleResponse(
|
|
||||||
response: ImportTopologySnapshotResponse
|
|
||||||
): Either[String, Unit] = Right(())
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class SignTransactions(
|
|
||||||
transactions: Seq[GenericSignedTopologyTransactionX],
|
|
||||||
signedBy: Seq[Fingerprint],
|
|
||||||
) extends BaseWriteCommand[SignTransactionsRequest, SignTransactionsResponse, Seq[
|
|
||||||
GenericSignedTopologyTransactionX
|
|
||||||
]] {
|
|
||||||
override def createRequest(): Either[String, SignTransactionsRequest] = {
|
|
||||||
Right(
|
|
||||||
SignTransactionsRequest(transactions.map(_.toProtoV30), signedBy.map(_.toProtoPrimitive))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerWriteServiceStub,
|
|
||||||
request: SignTransactionsRequest,
|
|
||||||
): Future[SignTransactionsResponse] = service.signTransactions(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: SignTransactionsResponse
|
|
||||||
): Either[String, Seq[GenericSignedTopologyTransactionX]] =
|
|
||||||
response.transactions
|
|
||||||
.traverse(tx =>
|
|
||||||
SignedTopologyTransactionX.fromProtoV30(ProtocolVersionValidation.NoValidation, tx)
|
|
||||||
)
|
|
||||||
.leftMap(_.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Propose[M <: TopologyMappingX: ClassTag](
|
|
||||||
mapping: Either[String, M],
|
|
||||||
signedBy: Seq[Fingerprint],
|
|
||||||
change: TopologyChangeOpX,
|
|
||||||
serial: Option[PositiveInt],
|
|
||||||
mustFullyAuthorize: Boolean,
|
|
||||||
forceChange: Boolean,
|
|
||||||
store: String,
|
|
||||||
) extends BaseWriteCommand[
|
|
||||||
AuthorizeRequest,
|
|
||||||
AuthorizeResponse,
|
|
||||||
SignedTopologyTransactionX[TopologyChangeOpX, M],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, AuthorizeRequest] = mapping.map(m =>
|
|
||||||
AuthorizeRequest(
|
|
||||||
Proposal(
|
|
||||||
AuthorizeRequest.Proposal(
|
|
||||||
change.toProto,
|
|
||||||
serial.map(_.value).getOrElse(0),
|
|
||||||
Some(m.toProtoV30),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
mustFullyAuthorize = mustFullyAuthorize,
|
|
||||||
forceChange = forceChange,
|
|
||||||
signedBy = signedBy.map(_.toProtoPrimitive),
|
|
||||||
store,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerWriteServiceStub,
|
|
||||||
request: AuthorizeRequest,
|
|
||||||
): Future[AuthorizeResponse] = service.authorize(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: AuthorizeResponse
|
|
||||||
): Either[String, SignedTopologyTransactionX[TopologyChangeOpX, M]] = response.transaction
|
|
||||||
.toRight("no transaction in response")
|
|
||||||
.flatMap(
|
|
||||||
SignedTopologyTransactionX
|
|
||||||
.fromProtoV30(ProtocolVersionValidation.NoValidation, _)
|
|
||||||
.leftMap(_.message)
|
|
||||||
.flatMap(tx =>
|
|
||||||
tx.selectMapping[M]
|
|
||||||
.toRight(
|
|
||||||
s"Expected mapping ${ClassTag[M].getClass.getSimpleName}, but received: ${tx.mapping.getClass.getSimpleName}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
object Propose {
|
|
||||||
def apply[M <: TopologyMappingX: ClassTag](
|
|
||||||
mapping: M,
|
|
||||||
signedBy: Seq[Fingerprint],
|
|
||||||
store: String,
|
|
||||||
serial: Option[PositiveInt] = None,
|
|
||||||
change: TopologyChangeOpX = TopologyChangeOpX.Replace,
|
|
||||||
mustFullyAuthorize: Boolean = false,
|
|
||||||
forceChange: Boolean = false,
|
|
||||||
): Propose[M] =
|
|
||||||
Propose(Right(mapping), signedBy, change, serial, mustFullyAuthorize, forceChange, store)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class Authorize[M <: TopologyMappingX: ClassTag](
|
|
||||||
transactionHash: String,
|
|
||||||
mustFullyAuthorize: Boolean,
|
|
||||||
signedBy: Seq[Fingerprint],
|
|
||||||
store: String,
|
|
||||||
) extends BaseWriteCommand[
|
|
||||||
AuthorizeRequest,
|
|
||||||
AuthorizeResponse,
|
|
||||||
SignedTopologyTransactionX[TopologyChangeOpX, M],
|
|
||||||
] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, AuthorizeRequest] = Right(
|
|
||||||
AuthorizeRequest(
|
|
||||||
TransactionHash(transactionHash),
|
|
||||||
mustFullyAuthorize = mustFullyAuthorize,
|
|
||||||
forceChange = false,
|
|
||||||
signedBy = signedBy.map(_.toProtoPrimitive),
|
|
||||||
store = store,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: TopologyManagerWriteServiceStub,
|
|
||||||
request: AuthorizeRequest,
|
|
||||||
): Future[AuthorizeResponse] = service.authorize(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: AuthorizeResponse
|
|
||||||
): Either[String, SignedTopologyTransactionX[TopologyChangeOpX, M]] = response.transaction
|
|
||||||
.toRight("no transaction in response")
|
|
||||||
.flatMap(
|
|
||||||
SignedTopologyTransactionX
|
|
||||||
.fromProtoV30(ProtocolVersionValidation.NoValidation, _)
|
|
||||||
.leftMap(_.message)
|
|
||||||
.flatMap(tx =>
|
|
||||||
tx.selectMapping[M]
|
|
||||||
.toRight(
|
|
||||||
s"Expected mapping ${ClassTag[M].getClass.getSimpleName}, but received: ${tx.mapping.getClass.getSimpleName}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Init {
|
|
||||||
|
|
||||||
abstract class BaseInitializationService[Req, Resp, Res]
|
|
||||||
extends GrpcAdminCommand[Req, Resp, Res] {
|
|
||||||
override type Svc = IdentityInitializationXServiceStub
|
|
||||||
override def createService(channel: ManagedChannel): IdentityInitializationXServiceStub =
|
|
||||||
v30.IdentityInitializationXServiceGrpc.stub(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class InitId(identifier: String)
|
|
||||||
extends BaseInitializationService[v30.InitIdRequest, v30.InitIdResponse, Unit] {
|
|
||||||
|
|
||||||
override def createRequest(): Either[String, v30.InitIdRequest] =
|
|
||||||
Right(v30.InitIdRequest(identifier))
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: IdentityInitializationXServiceStub,
|
|
||||||
request: v30.InitIdRequest,
|
|
||||||
): Future[v30.InitIdResponse] =
|
|
||||||
service.initId(request)
|
|
||||||
|
|
||||||
override def handleResponse(response: v30.InitIdResponse): Either[String, Unit] =
|
|
||||||
Right(())
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class GetId()
|
|
||||||
extends BaseInitializationService[v30.GetIdRequest, v30.GetIdResponse, UniqueIdentifier] {
|
|
||||||
override def createRequest(): Either[String, v30.GetIdRequest] =
|
|
||||||
Right(v30.GetIdRequest())
|
|
||||||
|
|
||||||
override def submitRequest(
|
|
||||||
service: IdentityInitializationXServiceStub,
|
|
||||||
request: v30.GetIdRequest,
|
|
||||||
): Future[v30.GetIdResponse] =
|
|
||||||
service.getId(request)
|
|
||||||
|
|
||||||
override def handleResponse(
|
|
||||||
response: v30.GetIdResponse
|
|
||||||
): Either[String, UniqueIdentifier] = {
|
|
||||||
if (response.uniqueIdentifier.nonEmpty)
|
|
||||||
UniqueIdentifier.fromProtoPrimitive_(response.uniqueIdentifier)
|
|
||||||
else
|
|
||||||
Left(
|
|
||||||
s"Node is not initialized and therefore does not have an Id assigned yet."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ import better.files.File
|
|||||||
import ch.qos.logback.classic.Level
|
import ch.qos.logback.classic.Level
|
||||||
import com.digitalasset.canton.admin.api.client.commands.{
|
import com.digitalasset.canton.admin.api.client.commands.{
|
||||||
StatusAdminCommands,
|
StatusAdminCommands,
|
||||||
TopologyAdminCommandsX,
|
TopologyAdminCommands,
|
||||||
}
|
}
|
||||||
import com.digitalasset.canton.config.{ConsoleCommandTimeout, NonNegativeDuration}
|
import com.digitalasset.canton.config.{ConsoleCommandTimeout, NonNegativeDuration}
|
||||||
import com.digitalasset.canton.console.CommandErrors.{CommandError, GenericCommandError}
|
import com.digitalasset.canton.console.CommandErrors.{CommandError, GenericCommandError}
|
||||||
@ -188,7 +188,7 @@ class HealthAdministrationX[S <: data.NodeStatus.Status](
|
|||||||
|
|
||||||
override def has_identity(): Boolean = runner
|
override def has_identity(): Boolean = runner
|
||||||
.adminCommand(
|
.adminCommand(
|
||||||
TopologyAdminCommandsX.Init.GetId()
|
TopologyAdminCommands.Init.GetId()
|
||||||
)
|
)
|
||||||
.toEither
|
.toEither
|
||||||
.isRight
|
.isRight
|
||||||
|
@ -98,6 +98,16 @@ private[console] object ParticipantCommands {
|
|||||||
.UploadDar(Some(path), vetAllPackages, synchronizeVetting, logger)
|
.UploadDar(Some(path), vetAllPackages, synchronizeVetting, logger)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
runner: AdminCommandRunner,
|
||||||
|
path: String,
|
||||||
|
logger: TracedLogger,
|
||||||
|
): ConsoleCommandResult[String] =
|
||||||
|
runner.adminCommand(
|
||||||
|
ParticipantAdminCommands.Package
|
||||||
|
.ValidateDar(Some(path), logger)
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object domains {
|
object domains {
|
||||||
|
@ -10,7 +10,6 @@ import com.digitalasset.canton.LedgerParticipantId
|
|||||||
import com.digitalasset.canton.admin.api.client.commands.{
|
import com.digitalasset.canton.admin.api.client.commands.{
|
||||||
ParticipantAdminCommands,
|
ParticipantAdminCommands,
|
||||||
TopologyAdminCommands,
|
TopologyAdminCommands,
|
||||||
TopologyAdminCommandsX,
|
|
||||||
}
|
}
|
||||||
import com.digitalasset.canton.admin.api.client.data.{
|
import com.digitalasset.canton.admin.api.client.data.{
|
||||||
ListConnectedDomainsResult,
|
ListConnectedDomainsResult,
|
||||||
@ -307,7 +306,7 @@ class ParticipantPartiesAdministrationGroupX(
|
|||||||
|
|
||||||
runner
|
runner
|
||||||
.adminCommand(
|
.adminCommand(
|
||||||
TopologyAdminCommandsX.Write.Propose(
|
TopologyAdminCommands.Write.Propose(
|
||||||
// TODO(#14048) properly set the serial or introduce auto-detection so we don't
|
// TODO(#14048) properly set the serial or introduce auto-detection so we don't
|
||||||
// have to set it on the client side
|
// have to set it on the client side
|
||||||
mapping = PartyToParticipantX(
|
mapping = PartyToParticipantX(
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -135,7 +135,7 @@ class SecretKeyAdministration(
|
|||||||
|
|
||||||
private def findPublicKey(
|
private def findPublicKey(
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
topologyAdmin: TopologyAdministrationGroupCommon,
|
topologyAdmin: TopologyAdministrationGroup,
|
||||||
owner: Member,
|
owner: Member,
|
||||||
): PublicKey =
|
): PublicKey =
|
||||||
findPublicKeys(topologyAdmin, owner).find(_.fingerprint.unwrap == fingerprint) match {
|
findPublicKeys(topologyAdmin, owner).find(_.fingerprint.unwrap == fingerprint) match {
|
||||||
@ -241,24 +241,17 @@ class SecretKeyAdministration(
|
|||||||
/** Helper to find public keys for topology/x shared between community and enterprise
|
/** Helper to find public keys for topology/x shared between community and enterprise
|
||||||
*/
|
*/
|
||||||
protected def findPublicKeys(
|
protected def findPublicKeys(
|
||||||
topologyAdmin: TopologyAdministrationGroupCommon,
|
topologyAdmin: TopologyAdministrationGroup,
|
||||||
owner: Member,
|
owner: Member,
|
||||||
): Seq[PublicKey] =
|
): Seq[PublicKey] = {
|
||||||
topologyAdmin match {
|
topologyAdmin.owner_to_key_mappings
|
||||||
case tx: TopologyAdministrationGroup =>
|
.list(
|
||||||
tx.owner_to_key_mappings
|
filterStore = AuthorizedStore.filterName,
|
||||||
.list(
|
filterKeyOwnerUid = owner.filterString,
|
||||||
filterStore = AuthorizedStore.filterName,
|
filterKeyOwnerType = Some(owner.code),
|
||||||
filterKeyOwnerUid = owner.filterString,
|
)
|
||||||
filterKeyOwnerType = Some(owner.code),
|
.flatMap(_.item.keys)
|
||||||
)
|
}
|
||||||
.flatMap(_.item.keys)
|
|
||||||
case _ =>
|
|
||||||
// TODO(#15161): Remove the match when flattening TopologyAdministrationGroup and Common
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"Impossible to encounter topology admin group besides X"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Helper to name new keys generated during a rotation with a ...-rotated-<timestamp> tag to better identify
|
/** Helper to name new keys generated during a rotation with a ...-rotated-<timestamp> tag to better identify
|
||||||
* the new keys after a rotation
|
* the new keys after a rotation
|
||||||
|
@ -5,7 +5,6 @@ package com.digitalasset.canton.sequencing.protocol
|
|||||||
|
|
||||||
import cats.Functor
|
import cats.Functor
|
||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
import cats.syntax.either.*
|
|
||||||
import cats.syntax.traverse.*
|
import cats.syntax.traverse.*
|
||||||
import com.daml.nonempty.NonEmpty
|
import com.daml.nonempty.NonEmpty
|
||||||
import com.digitalasset.canton.checked
|
import com.digitalasset.canton.checked
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package com.digitalasset.canton.topology.store
|
||||||
|
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.LengthLimitedString.DisplayName
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.String255
|
||||||
|
import com.digitalasset.canton.config.ProcessingTimeout
|
||||||
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
|
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||||
|
import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage}
|
||||||
|
import com.digitalasset.canton.topology.*
|
||||||
|
import com.digitalasset.canton.topology.store.db.DbPartyMetadataStore
|
||||||
|
import com.digitalasset.canton.topology.store.memory.InMemoryPartyMetadataStore
|
||||||
|
import com.digitalasset.canton.tracing.TraceContext
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
/** the party metadata used to inform the ledger api server
|
||||||
|
*
|
||||||
|
* the first class parameters correspond to the relevant information, whereas the
|
||||||
|
* second class parameters are synchronisation information used during crash recovery.
|
||||||
|
* we don't want these in an equality comparison.
|
||||||
|
*/
|
||||||
|
final case class PartyMetadata(
|
||||||
|
partyId: PartyId,
|
||||||
|
displayName: Option[DisplayName],
|
||||||
|
participantId: Option[ParticipantId],
|
||||||
|
)(
|
||||||
|
val effectiveTimestamp: CantonTimestamp,
|
||||||
|
val submissionId: String255,
|
||||||
|
val notified: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
trait PartyMetadataStore extends AutoCloseable {
|
||||||
|
|
||||||
|
def metadataForParty(partyId: PartyId)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[PartyMetadata]]
|
||||||
|
|
||||||
|
final def insertOrUpdatePartyMetadata(metadata: PartyMetadata)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Unit] = {
|
||||||
|
insertOrUpdatePartyMetadata(
|
||||||
|
partyId = metadata.partyId,
|
||||||
|
participantId = metadata.participantId,
|
||||||
|
displayName = metadata.displayName,
|
||||||
|
effectiveTimestamp = metadata.effectiveTimestamp,
|
||||||
|
submissionId = metadata.submissionId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def insertOrUpdatePartyMetadata(
|
||||||
|
partyId: PartyId,
|
||||||
|
participantId: Option[ParticipantId],
|
||||||
|
displayName: Option[DisplayName],
|
||||||
|
effectiveTimestamp: CantonTimestamp,
|
||||||
|
submissionId: String255,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit]
|
||||||
|
|
||||||
|
/** mark the given metadata as having been successfully forwarded to the domain */
|
||||||
|
def markNotified(metadata: PartyMetadata)(implicit traceContext: TraceContext): Future[Unit]
|
||||||
|
|
||||||
|
/** fetch the current set of party data which still needs to be notified */
|
||||||
|
def fetchNotNotified()(implicit traceContext: TraceContext): Future[Seq[PartyMetadata]]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object PartyMetadataStore {
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
storage: Storage,
|
||||||
|
timeouts: ProcessingTimeout,
|
||||||
|
loggerFactory: NamedLoggerFactory,
|
||||||
|
)(implicit
|
||||||
|
ec: ExecutionContext
|
||||||
|
): PartyMetadataStore =
|
||||||
|
storage match {
|
||||||
|
case _: MemoryStorage => new InMemoryPartyMetadataStore()
|
||||||
|
case jdbc: DbStorage => new DbPartyMetadataStore(jdbc, timeouts, loggerFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,93 +3,46 @@
|
|||||||
|
|
||||||
package com.digitalasset.canton.topology.store
|
package com.digitalasset.canton.topology.store
|
||||||
|
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.LengthLimitedString.DisplayName
|
import cats.syntax.traverse.*
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.{
|
import com.daml.nonempty.NonEmpty
|
||||||
LengthLimitedString,
|
import com.digitalasset.canton.ProtoDeserializationError
|
||||||
String255,
|
import com.digitalasset.canton.config.CantonRequireTypes.{LengthLimitedString, String255}
|
||||||
String256M,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.config.ProcessingTimeout
|
import com.digitalasset.canton.config.ProcessingTimeout
|
||||||
import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong}
|
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||||
import com.digitalasset.canton.crypto.{Fingerprint, SignatureCheckError}
|
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
|
import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown}
|
||||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||||
import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory}
|
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||||
import com.digitalasset.canton.protocol.OnboardingRestriction
|
|
||||||
import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage}
|
import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage}
|
||||||
|
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||||
|
import com.digitalasset.canton.time.NonNegativeFiniteDuration
|
||||||
import com.digitalasset.canton.topology.*
|
import com.digitalasset.canton.topology.*
|
||||||
import com.digitalasset.canton.topology.store.db.DbPartyMetadataStore
|
import com.digitalasset.canton.topology.admin.v30 as topoV30
|
||||||
import com.digitalasset.canton.topology.store.memory.InMemoryPartyMetadataStore
|
import com.digitalasset.canton.topology.client.DomainTopologyClient
|
||||||
|
import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime}
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionX.GenericStoredTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.{
|
||||||
|
GenericStoredTopologyTransactionsX,
|
||||||
|
PositiveStoredTopologyTransactionsX,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.store.TopologyTransactionRejection.Duplicate
|
||||||
|
import com.digitalasset.canton.topology.store.ValidatedTopologyTransactionX.GenericValidatedTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.store.db.DbTopologyStoreX
|
||||||
|
import com.digitalasset.canton.topology.store.memory.InMemoryTopologyStoreX
|
||||||
|
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyTransactionX.{
|
||||||
|
GenericTopologyTransactionX,
|
||||||
|
TxHash,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.transaction.*
|
||||||
import com.digitalasset.canton.tracing.TraceContext
|
import com.digitalasset.canton.tracing.TraceContext
|
||||||
|
import com.digitalasset.canton.version.ProtocolVersion
|
||||||
|
import com.google.common.annotations.VisibleForTesting
|
||||||
|
|
||||||
|
import scala.concurrent.duration.Duration
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.reflect.ClassTag
|
||||||
// TODO(#15161): Rename file to PartyMetadata or split up helpers into multiple files
|
|
||||||
/** the party metadata used to inform the ledger api server
|
|
||||||
*
|
|
||||||
* the first class parameters correspond to the relevant information, whereas the
|
|
||||||
* second class parameters are synchronisation information used during crash recovery.
|
|
||||||
* we don't want these in an equality comparison.
|
|
||||||
*/
|
|
||||||
final case class PartyMetadata(
|
|
||||||
partyId: PartyId,
|
|
||||||
displayName: Option[DisplayName],
|
|
||||||
participantId: Option[ParticipantId],
|
|
||||||
)(
|
|
||||||
val effectiveTimestamp: CantonTimestamp,
|
|
||||||
val submissionId: String255,
|
|
||||||
val notified: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
trait PartyMetadataStore extends AutoCloseable {
|
|
||||||
|
|
||||||
def metadataForParty(partyId: PartyId)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[PartyMetadata]]
|
|
||||||
|
|
||||||
final def insertOrUpdatePartyMetadata(metadata: PartyMetadata)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Unit] = {
|
|
||||||
insertOrUpdatePartyMetadata(
|
|
||||||
partyId = metadata.partyId,
|
|
||||||
participantId = metadata.participantId,
|
|
||||||
displayName = metadata.displayName,
|
|
||||||
effectiveTimestamp = metadata.effectiveTimestamp,
|
|
||||||
submissionId = metadata.submissionId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def insertOrUpdatePartyMetadata(
|
|
||||||
partyId: PartyId,
|
|
||||||
participantId: Option[ParticipantId],
|
|
||||||
displayName: Option[DisplayName],
|
|
||||||
effectiveTimestamp: CantonTimestamp,
|
|
||||||
submissionId: String255,
|
|
||||||
)(implicit traceContext: TraceContext): Future[Unit]
|
|
||||||
|
|
||||||
/** mark the given metadata as having been successfully forwarded to the domain */
|
|
||||||
def markNotified(metadata: PartyMetadata)(implicit traceContext: TraceContext): Future[Unit]
|
|
||||||
|
|
||||||
/** fetch the current set of party data which still needs to be notified */
|
|
||||||
def fetchNotNotified()(implicit traceContext: TraceContext): Future[Seq[PartyMetadata]]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object PartyMetadataStore {
|
|
||||||
|
|
||||||
def apply(
|
|
||||||
storage: Storage,
|
|
||||||
timeouts: ProcessingTimeout,
|
|
||||||
loggerFactory: NamedLoggerFactory,
|
|
||||||
)(implicit
|
|
||||||
ec: ExecutionContext
|
|
||||||
): PartyMetadataStore =
|
|
||||||
storage match {
|
|
||||||
case _: MemoryStorage => new InMemoryPartyMetadataStore()
|
|
||||||
case jdbc: DbStorage => new DbPartyMetadataStore(jdbc, timeouts, loggerFactory)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed trait TopologyStoreId extends PrettyPrinting {
|
sealed trait TopologyStoreId extends PrettyPrinting {
|
||||||
def filterName: String = dbString.unwrap
|
def filterName: String = dbString.unwrap
|
||||||
@ -182,150 +135,417 @@ object TopologyStoreId {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed trait TopologyTransactionRejection extends PrettyPrinting with Product with Serializable {
|
final case class StoredTopologyTransactionX[+Op <: TopologyChangeOpX, +M <: TopologyMappingX](
|
||||||
def asString: String
|
sequenced: SequencedTime,
|
||||||
def asString1GB: String256M =
|
validFrom: EffectiveTime,
|
||||||
String256M.tryCreate(asString, Some("topology transaction rejection"))
|
validUntil: Option[EffectiveTime],
|
||||||
|
transaction: SignedTopologyTransactionX[Op, M],
|
||||||
|
) extends DelegatedTopologyTransactionLike[Op, M]
|
||||||
|
with PrettyPrinting {
|
||||||
|
override protected def transactionLikeDelegate: TopologyTransactionLike[Op, M] = transaction
|
||||||
|
|
||||||
def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError
|
override def pretty: Pretty[StoredTopologyTransactionX.this.type] =
|
||||||
}
|
prettyOfClass(
|
||||||
object TopologyTransactionRejection {
|
unnamedParam(_.transaction),
|
||||||
|
param("sequenced", _.sequenced.value),
|
||||||
final case class NoDelegationFoundForKeys(keys: Set[Fingerprint])
|
param("validFrom", _.validFrom.value),
|
||||||
extends TopologyTransactionRejection {
|
paramIfDefined("validUntil", _.validUntil.map(_.value)),
|
||||||
override def asString: String = s"No delegation found for keys ${keys.mkString(", ")}"
|
|
||||||
override def pretty: Pretty[NoDelegationFoundForKeys] = prettyOfString(_ => asString)
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
|
||||||
TopologyManagerError.UnauthorizedTransaction.Failure(asString)
|
|
||||||
|
|
||||||
}
|
|
||||||
case object NotAuthorized extends TopologyTransactionRejection {
|
|
||||||
override def asString: String = "Not authorized"
|
|
||||||
override def pretty: Pretty[NotAuthorized.type] = prettyOfString(_ => asString)
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
|
||||||
TopologyManagerError.UnauthorizedTransaction.Failure(asString)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ThresholdTooHigh(actual: Int, mustBeAtMost: Int)
|
|
||||||
extends TopologyTransactionRejection {
|
|
||||||
override def asString: String =
|
|
||||||
s"Threshold must not be higher than $mustBeAtMost, but was $actual."
|
|
||||||
|
|
||||||
override def pretty: Pretty[ThresholdTooHigh] = prettyOfString(_ => asString)
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) = {
|
|
||||||
TopologyManagerError.InvalidThreshold.ThresholdTooHigh(actual, mustBeAtMost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class OnboardingRestrictionInPlace(
|
|
||||||
participant: ParticipantId,
|
|
||||||
restriction: OnboardingRestriction,
|
|
||||||
loginAfter: Option[CantonTimestamp],
|
|
||||||
) extends TopologyTransactionRejection {
|
|
||||||
override def asString: String =
|
|
||||||
s"Participant ${participant} onboarding rejected as restrictions ${restriction} are in place."
|
|
||||||
|
|
||||||
override def pretty: Pretty[OnboardingRestrictionInPlace] = prettyOfString(_ => asString)
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) = {
|
|
||||||
TopologyManagerError.ParticipantOnboardingRefused.Reject(participant, restriction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class SignatureCheckFailed(err: SignatureCheckError)
|
|
||||||
extends TopologyTransactionRejection {
|
|
||||||
override def asString: String = err.toString
|
|
||||||
override def pretty: Pretty[SignatureCheckFailed] = prettyOfClass(param("err", _.err))
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
|
||||||
TopologyManagerError.InvalidSignatureError.Failure(err)
|
|
||||||
}
|
|
||||||
final case class WrongDomain(wrong: DomainId) extends TopologyTransactionRejection {
|
|
||||||
override def asString: String = show"Wrong domain $wrong"
|
|
||||||
override def pretty: Pretty[WrongDomain] = prettyOfClass(param("wrong", _.wrong))
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
|
||||||
TopologyManagerError.WrongDomain.Failure(wrong)
|
|
||||||
}
|
|
||||||
final case class Duplicate(old: CantonTimestamp) extends TopologyTransactionRejection {
|
|
||||||
override def asString: String = show"Duplicate transaction from ${old}"
|
|
||||||
override def pretty: Pretty[Duplicate] = prettyOfClass(param("old", _.old))
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
|
||||||
TopologyManagerError.DuplicateTransaction.ExistsAt(old)
|
|
||||||
}
|
|
||||||
final case class SerialMismatch(expected: PositiveInt, actual: PositiveInt)
|
|
||||||
extends TopologyTransactionRejection {
|
|
||||||
override def asString: String =
|
|
||||||
show"The given serial $actual does not match the expected serial $expected"
|
|
||||||
override def pretty: Pretty[SerialMismatch] =
|
|
||||||
prettyOfClass(param("expected", _.expected), param("actual", _.actual))
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
|
||||||
TopologyManagerError.SerialMismatch.Failure(expected, actual)
|
|
||||||
}
|
|
||||||
final case class Other(str: String) extends TopologyTransactionRejection {
|
|
||||||
override def asString: String = str
|
|
||||||
override def pretty: Pretty[Other] = prettyOfString(_ => asString)
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
|
||||||
TopologyManagerError.InternalError.Other(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ExtraTrafficLimitTooLow(
|
|
||||||
member: Member,
|
|
||||||
actual: PositiveLong,
|
|
||||||
expectedMinimum: PositiveLong,
|
|
||||||
) extends TopologyTransactionRejection {
|
|
||||||
override def asString: String =
|
|
||||||
s"Extra traffic limit for $member should be at least $expectedMinimum, but was $actual."
|
|
||||||
|
|
||||||
override def pretty: Pretty[ExtraTrafficLimitTooLow] =
|
|
||||||
prettyOfClass(
|
|
||||||
param("member", _.member),
|
|
||||||
param("actual", _.actual),
|
|
||||||
param("expectedMinimum", _.expectedMinimum),
|
|
||||||
)
|
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
|
||||||
TopologyManagerError.InvalidTrafficLimit.TrafficLimitTooLow(member, actual, expectedMinimum)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class InsufficientKeys(members: Seq[Member]) extends TopologyTransactionRejection {
|
|
||||||
override def asString: String =
|
|
||||||
s"Members ${members.sorted.mkString(", ")} are missing a signing key or an encryption key or both."
|
|
||||||
|
|
||||||
override def pretty: Pretty[InsufficientKeys] = prettyOfClass(
|
|
||||||
param("members", _.members)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||||
TopologyManagerError.InsufficientKeys.Failure(members)
|
def selectMapping[TargetMapping <: TopologyMappingX: ClassTag] = transaction
|
||||||
|
.selectMapping[TargetMapping]
|
||||||
|
.map(_ => this.asInstanceOf[StoredTopologyTransactionX[Op, TargetMapping]])
|
||||||
|
|
||||||
|
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||||
|
def selectOp[TargetOp <: TopologyChangeOpX: ClassTag] = transaction
|
||||||
|
.selectOp[TargetOp]
|
||||||
|
.map(_ => this.asInstanceOf[StoredTopologyTransactionX[TargetOp, M]])
|
||||||
|
}
|
||||||
|
|
||||||
|
object StoredTopologyTransactionX {
|
||||||
|
type GenericStoredTopologyTransactionX =
|
||||||
|
StoredTopologyTransactionX[TopologyChangeOpX, TopologyMappingX]
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ValidatedTopologyTransactionX[+Op <: TopologyChangeOpX, +M <: TopologyMappingX](
|
||||||
|
transaction: SignedTopologyTransactionX[Op, M],
|
||||||
|
rejectionReason: Option[TopologyTransactionRejection] = None,
|
||||||
|
expireImmediately: Boolean = false,
|
||||||
|
) extends DelegatedTopologyTransactionLike[Op, M]
|
||||||
|
with PrettyPrinting {
|
||||||
|
|
||||||
|
override protected def transactionLikeDelegate: TopologyTransactionLike[Op, M] = transaction
|
||||||
|
|
||||||
|
def nonDuplicateRejectionReason: Option[TopologyTransactionRejection] = rejectionReason match {
|
||||||
|
case Some(Duplicate(_)) => None
|
||||||
|
case otherwise => otherwise
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class UnknownMembers(members: Seq[Member]) extends TopologyTransactionRejection {
|
def collectOfMapping[TargetM <: TopologyMappingX: ClassTag]
|
||||||
override def asString: String = s"Members ${members.toSeq.sorted.mkString(", ")} are unknown."
|
: Option[ValidatedTopologyTransactionX[Op, TargetM]] =
|
||||||
|
transaction.selectMapping[TargetM].map(tx => copy[Op, TargetM](transaction = tx))
|
||||||
|
|
||||||
override def pretty: Pretty[UnknownMembers] = prettyOfClass(param("members", _.members))
|
def collectOf[TargetO <: TopologyChangeOpX: ClassTag, TargetM <: TopologyMappingX: ClassTag]
|
||||||
|
: Option[ValidatedTopologyTransactionX[TargetO, TargetM]] =
|
||||||
|
transaction.select[TargetO, TargetM].map(tx => copy[TargetO, TargetM](transaction = tx))
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
override def pretty: Pretty[ValidatedTopologyTransactionX.this.type] =
|
||||||
TopologyManagerError.UnknownMembers.Failure(members)
|
prettyOfClass(
|
||||||
|
unnamedParam(_.transaction),
|
||||||
|
paramIfDefined("rejectionReason", _.rejectionReason),
|
||||||
|
paramIfTrue("expireImmediately", _.expireImmediately),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object ValidatedTopologyTransactionX {
|
||||||
|
type GenericValidatedTopologyTransactionX =
|
||||||
|
ValidatedTopologyTransactionX[TopologyChangeOpX, TopologyMappingX]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class TopologyStoreX[+StoreID <: TopologyStoreId](implicit
|
||||||
|
protected val ec: ExecutionContext
|
||||||
|
) extends FlagCloseable {
|
||||||
|
this: NamedLogging =>
|
||||||
|
|
||||||
|
def storeId: 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 "pokes" of the
|
||||||
|
* topology client and updates of the acs commitment processor on startup
|
||||||
|
*/
|
||||||
|
def findUpcomingEffectiveChanges(asOfInclusive: CantonTimestamp)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[TopologyStoreX.Change]]
|
||||||
|
|
||||||
|
def maxTimestamp()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[(SequencedTime, EffectiveTime)]]
|
||||||
|
|
||||||
|
/** returns the current dispatching watermark
|
||||||
|
*
|
||||||
|
* for topology transaction dispatching, we keep track up to which point in time
|
||||||
|
* we have mirrored the authorized store to the remote store
|
||||||
|
*
|
||||||
|
* the timestamp always refers to the timestamp of the authorized store!
|
||||||
|
*/
|
||||||
|
def currentDispatchingWatermark(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[CantonTimestamp]]
|
||||||
|
|
||||||
|
/** update the dispatching watermark for this target store */
|
||||||
|
def updateDispatchingWatermark(timestamp: CantonTimestamp)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Unit]
|
||||||
|
|
||||||
|
def findTransactionsByTxHash(asOfExclusive: EffectiveTime, hashes: Set[TxHash])(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]]
|
||||||
|
|
||||||
|
def findProposalsByTxHash(asOfExclusive: EffectiveTime, hashes: NonEmpty[Set[TxHash]])(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]]
|
||||||
|
|
||||||
|
def findTransactionsForMapping(asOfExclusive: EffectiveTime, hashes: NonEmpty[Set[MappingHash]])(
|
||||||
|
implicit traceContext: TraceContext
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]]
|
||||||
|
|
||||||
|
/** returns the set of positive transactions
|
||||||
|
*
|
||||||
|
* this function is used by the topology processor to determine the set of transaction, such that
|
||||||
|
* we can perform cascading updates if there was a certificate revocation
|
||||||
|
*
|
||||||
|
* @param asOfInclusive whether the search interval should include the current timepoint or not. the state at t is
|
||||||
|
* defined as "exclusive" of t, whereas for updating the state, we need to be able to query inclusive.
|
||||||
|
*/
|
||||||
|
def findPositiveTransactions(
|
||||||
|
asOf: CantonTimestamp,
|
||||||
|
asOfInclusive: Boolean,
|
||||||
|
isProposal: Boolean,
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
filterUid: Option[Seq[UniqueIdentifier]],
|
||||||
|
filterNamespace: Option[Seq[Namespace]],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[PositiveStoredTopologyTransactionsX]
|
||||||
|
|
||||||
|
/** add validated topology transaction as is to the topology transaction table */
|
||||||
|
def update(
|
||||||
|
sequenced: SequencedTime,
|
||||||
|
effective: EffectiveTime,
|
||||||
|
removeMapping: Map[MappingHash, PositiveInt],
|
||||||
|
removeTxs: Set[TxHash],
|
||||||
|
additions: Seq[GenericValidatedTopologyTransactionX],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Unit]
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected[topology] def dumpStoreContent()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX]
|
||||||
|
|
||||||
|
/** store an initial set of topology transactions as given into the store */
|
||||||
|
def bootstrap(snapshot: GenericStoredTopologyTransactionsX)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Unit]
|
||||||
|
|
||||||
|
/** query optimized for inspection
|
||||||
|
*
|
||||||
|
* @param proposals if true, query only for proposals instead of approved transaction mappings
|
||||||
|
* @param recentTimestampO if exists, use this timestamp for the head state to prevent race conditions on the console
|
||||||
|
*/
|
||||||
|
def inspect(
|
||||||
|
proposals: Boolean,
|
||||||
|
timeQuery: TimeQuery,
|
||||||
|
// TODO(#14048) - consider removing `recentTimestampO` and moving callers to TimeQueryX.Snapshot
|
||||||
|
recentTimestampO: Option[CantonTimestamp],
|
||||||
|
op: Option[TopologyChangeOpX],
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
idFilter: Option[String],
|
||||||
|
namespaceFilter: Option[String],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]]
|
||||||
|
|
||||||
|
def inspectKnownParties(
|
||||||
|
timestamp: CantonTimestamp,
|
||||||
|
filterParty: String,
|
||||||
|
filterParticipant: String,
|
||||||
|
limit: Int,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Set[PartyId]]
|
||||||
|
|
||||||
|
/** Finds the topology transaction that first onboarded the sequencer with ID `sequencerId`
|
||||||
|
*/
|
||||||
|
def findFirstSequencerStateForSequencer(
|
||||||
|
sequencerId: SequencerId
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, SequencerDomainStateX]]]
|
||||||
|
|
||||||
|
/** Finds the topology transaction that first onboarded the mediator with ID `mediatorId`
|
||||||
|
*/
|
||||||
|
def findFirstMediatorStateForMediator(
|
||||||
|
mediatorId: MediatorId
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, MediatorDomainStateX]]]
|
||||||
|
|
||||||
|
/** Finds the topology transaction that first onboarded the participant with ID `participantId`
|
||||||
|
*/
|
||||||
|
def findFirstTrustCertificateForParticipant(
|
||||||
|
participant: ParticipantId
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, DomainTrustCertificateX]]]
|
||||||
|
|
||||||
|
def findEssentialStateAtSequencedTime(
|
||||||
|
asOfInclusive: SequencedTime
|
||||||
|
)(implicit traceContext: TraceContext): Future[GenericStoredTopologyTransactionsX]
|
||||||
|
|
||||||
|
protected def signedTxFromStoredTx(
|
||||||
|
storedTx: GenericStoredTopologyTransactionX
|
||||||
|
): SignedTopologyTransactionX[TopologyChangeOpX, TopologyMappingX] = storedTx.transaction
|
||||||
|
|
||||||
|
def providesAdditionalSignatures(
|
||||||
|
transaction: GenericSignedTopologyTransactionX
|
||||||
|
)(implicit traceContext: TraceContext): Future[Boolean] = {
|
||||||
|
findStored(CantonTimestamp.MaxValue, transaction).map(_.forall { inStore =>
|
||||||
|
// check whether source still could provide an additional signature
|
||||||
|
transaction.signatures.diff(inStore.transaction.signatures.forgetNE).nonEmpty &&
|
||||||
|
// but only if the transaction in the target store is a valid proposal
|
||||||
|
inStore.transaction.isProposal &&
|
||||||
|
inStore.validUntil.isEmpty
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class ParticipantStillHostsParties(participantId: ParticipantId, parties: Seq[PartyId])
|
/** returns initial set of onboarding transactions that should be dispatched to the domain */
|
||||||
extends TopologyTransactionRejection {
|
def findParticipantOnboardingTransactions(participantId: ParticipantId, domainId: DomainId)(
|
||||||
override def asString: String =
|
implicit traceContext: TraceContext
|
||||||
s"Cannot remove domain trust certificate for $participantId because it still hosts parties ${parties
|
): FutureUnlessShutdown[Seq[GenericSignedTopologyTransactionX]]
|
||||||
.mkString(",")}"
|
|
||||||
|
|
||||||
override def pretty: Pretty[ParticipantStillHostsParties] =
|
def findDispatchingTransactionsAfter(
|
||||||
prettyOfClass(param("participantId", _.participantId), param("parties", _.parties))
|
timestampExclusive: CantonTimestamp,
|
||||||
|
limit: Option[Int],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX]
|
||||||
|
|
||||||
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
def findStoredForVersion(
|
||||||
TopologyManagerError.IllegalRemovalOfDomainTrustCertificate.ParticipantStillHostsParties(
|
asOfExclusive: CantonTimestamp,
|
||||||
participantId,
|
transaction: GenericTopologyTransactionX,
|
||||||
parties,
|
protocolVersion: ProtocolVersion,
|
||||||
)
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[GenericStoredTopologyTransactionX]]
|
||||||
|
|
||||||
|
final def exists(transaction: GenericSignedTopologyTransactionX)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Boolean] = findStored(CantonTimestamp.MaxValue, transaction).map(
|
||||||
|
_.exists(signedTxFromStoredTx(_) == transaction)
|
||||||
|
)
|
||||||
|
|
||||||
|
def findStored(
|
||||||
|
asOfExclusive: CantonTimestamp,
|
||||||
|
transaction: GenericSignedTopologyTransactionX,
|
||||||
|
includeRejected: Boolean = false,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[GenericStoredTopologyTransactionX]]
|
||||||
|
}
|
||||||
|
|
||||||
|
object TopologyStoreX {
|
||||||
|
|
||||||
|
sealed trait Change extends Product with Serializable {
|
||||||
|
def sequenced: SequencedTime
|
||||||
|
def effective: EffectiveTime
|
||||||
|
}
|
||||||
|
|
||||||
|
object Change {
|
||||||
|
final case class TopologyDelay(
|
||||||
|
sequenced: SequencedTime,
|
||||||
|
effective: EffectiveTime,
|
||||||
|
epsilon: NonNegativeFiniteDuration,
|
||||||
|
) extends Change
|
||||||
|
|
||||||
|
final case class Other(sequenced: SequencedTime, effective: EffectiveTime) extends Change
|
||||||
|
}
|
||||||
|
|
||||||
|
def accumulateUpcomingEffectiveChanges(
|
||||||
|
items: Seq[StoredTopologyTransactionX[TopologyChangeOpX, TopologyMappingX]]
|
||||||
|
): Seq[Change] = {
|
||||||
|
items
|
||||||
|
.map(x => (x, x.mapping))
|
||||||
|
.map {
|
||||||
|
case (tx, x: DomainParametersStateX) =>
|
||||||
|
Change.TopologyDelay(tx.sequenced, tx.validFrom, x.parameters.topologyChangeDelay)
|
||||||
|
case (tx, _) => Change.Other(tx.sequenced, tx.validFrom)
|
||||||
|
}
|
||||||
|
.sortBy(_.effective)
|
||||||
|
.distinct
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply[StoreID <: TopologyStoreId](
|
||||||
|
storeId: StoreID,
|
||||||
|
storage: Storage,
|
||||||
|
timeouts: ProcessingTimeout,
|
||||||
|
loggerFactory: NamedLoggerFactory,
|
||||||
|
)(implicit
|
||||||
|
ec: ExecutionContext
|
||||||
|
): TopologyStoreX[StoreID] = {
|
||||||
|
val storeLoggerFactory = loggerFactory.append("store", storeId.toString)
|
||||||
|
storage match {
|
||||||
|
case _: MemoryStorage =>
|
||||||
|
new InMemoryTopologyStoreX(storeId, storeLoggerFactory, timeouts)
|
||||||
|
case dbStorage: DbStorage =>
|
||||||
|
new DbTopologyStoreX(dbStorage, storeId, timeouts, storeLoggerFactory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val initialParticipantDispatchingSet = Set(
|
||||||
|
TopologyMappingX.Code.DomainTrustCertificateX,
|
||||||
|
TopologyMappingX.Code.OwnerToKeyMappingX,
|
||||||
|
// TODO(#14060) - potentially revisit this once we implement TopologyStoreX.filterInitialParticipantDispatchingTransactions
|
||||||
|
TopologyMappingX.Code.NamespaceDelegationX,
|
||||||
|
TopologyMappingX.Code.IdentifierDelegationX,
|
||||||
|
TopologyMappingX.Code.DecentralizedNamespaceDefinitionX,
|
||||||
|
)
|
||||||
|
|
||||||
|
def filterInitialParticipantDispatchingTransactions(
|
||||||
|
participantId: ParticipantId,
|
||||||
|
domainId: DomainId,
|
||||||
|
transactions: Seq[GenericStoredTopologyTransactionX],
|
||||||
|
): Seq[GenericSignedTopologyTransactionX] = {
|
||||||
|
// TODO(#14060): Extend filtering along the lines of:
|
||||||
|
// TopologyStore.filterInitialParticipantDispatchingTransactions
|
||||||
|
transactions.map(_.transaction).collect {
|
||||||
|
case tx @ SignedTopologyTransactionX(
|
||||||
|
TopologyTransactionX(_, _, DomainTrustCertificateX(`participantId`, `domainId`, _, _)),
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) =>
|
||||||
|
tx
|
||||||
|
case tx @ SignedTopologyTransactionX(
|
||||||
|
TopologyTransactionX(_, _, OwnerToKeyMappingX(`participantId`, _, _)),
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) =>
|
||||||
|
tx
|
||||||
|
case tx @ SignedTopologyTransactionX(
|
||||||
|
TopologyTransactionX(_, _, NamespaceDelegationX(ns, _, _)),
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) if ns == participantId.uid.namespace =>
|
||||||
|
tx
|
||||||
|
case tx @ SignedTopologyTransactionX(
|
||||||
|
TopologyTransactionX(_, _, IdentifierDelegationX(uid, _)),
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) if uid == participantId.uid =>
|
||||||
|
tx
|
||||||
|
case tx @ SignedTopologyTransactionX(
|
||||||
|
TopologyTransactionX(_, _, _: DecentralizedNamespaceDefinitionX),
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) =>
|
||||||
|
tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** convenience method waiting until the last eligible transaction inserted into the source store has been dispatched successfully to the target domain */
|
||||||
|
def awaitTxObserved(
|
||||||
|
client: DomainTopologyClient,
|
||||||
|
transaction: GenericSignedTopologyTransactionX,
|
||||||
|
target: TopologyStoreX[?],
|
||||||
|
timeout: Duration,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext,
|
||||||
|
executionContext: ExecutionContext,
|
||||||
|
): FutureUnlessShutdown[Boolean] = {
|
||||||
|
client.await(
|
||||||
|
// we know that the transaction is stored and effective once we find it in the target
|
||||||
|
// domain store and once the effective time (valid from) is smaller than the client timestamp
|
||||||
|
sp => target.findStored(sp.timestamp, transaction, includeRejected = true).map(_.nonEmpty),
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed trait TimeQuery {
|
||||||
|
def toProtoV30: topoV30.BaseQuery.TimeQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
object TimeQuery {
|
||||||
|
object HeadState extends TimeQuery {
|
||||||
|
override def toProtoV30: topoV30.BaseQuery.TimeQuery =
|
||||||
|
topoV30.BaseQuery.TimeQuery.HeadState(com.google.protobuf.empty.Empty())
|
||||||
|
}
|
||||||
|
final case class Snapshot(asOf: CantonTimestamp) extends TimeQuery {
|
||||||
|
override def toProtoV30: topoV30.BaseQuery.TimeQuery =
|
||||||
|
topoV30.BaseQuery.TimeQuery.Snapshot(asOf.toProtoTimestamp)
|
||||||
|
}
|
||||||
|
final case class Range(from: Option[CantonTimestamp], until: Option[CantonTimestamp])
|
||||||
|
extends TimeQuery {
|
||||||
|
override def toProtoV30: topoV30.BaseQuery.TimeQuery = topoV30.BaseQuery.TimeQuery.Range(
|
||||||
|
topoV30.BaseQuery.TimeRange(from.map(_.toProtoTimestamp), until.map(_.toProtoTimestamp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def fromProto(
|
||||||
|
proto: topoV30.BaseQuery.TimeQuery,
|
||||||
|
fieldName: String,
|
||||||
|
): ParsingResult[TimeQuery] =
|
||||||
|
proto match {
|
||||||
|
case topoV30.BaseQuery.TimeQuery.Empty =>
|
||||||
|
Left(ProtoDeserializationError.FieldNotSet(fieldName))
|
||||||
|
case topoV30.BaseQuery.TimeQuery.Snapshot(value) =>
|
||||||
|
CantonTimestamp.fromProtoTimestamp(value).map(Snapshot)
|
||||||
|
case topoV30.BaseQuery.TimeQuery.HeadState(_) => Right(HeadState)
|
||||||
|
case topoV30.BaseQuery.TimeQuery.Range(value) =>
|
||||||
|
for {
|
||||||
|
fromO <- value.from.traverse(CantonTimestamp.fromProtoTimestamp)
|
||||||
|
toO <- value.until.traverse(CantonTimestamp.fromProtoTimestamp)
|
||||||
|
} yield Range(fromO, toO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,459 +0,0 @@
|
|||||||
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package com.digitalasset.canton.topology.store
|
|
||||||
|
|
||||||
import cats.syntax.traverse.*
|
|
||||||
import com.daml.nonempty.NonEmpty
|
|
||||||
import com.digitalasset.canton.ProtoDeserializationError
|
|
||||||
import com.digitalasset.canton.config.ProcessingTimeout
|
|
||||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
|
||||||
import com.digitalasset.canton.lifecycle.{FlagCloseable, FutureUnlessShutdown}
|
|
||||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
|
||||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
|
||||||
import com.digitalasset.canton.resource.{DbStorage, MemoryStorage, Storage}
|
|
||||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
|
||||||
import com.digitalasset.canton.time.NonNegativeFiniteDuration
|
|
||||||
import com.digitalasset.canton.topology.*
|
|
||||||
import com.digitalasset.canton.topology.admin.v30 as topoV30
|
|
||||||
import com.digitalasset.canton.topology.client.DomainTopologyClient
|
|
||||||
import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime}
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionX.GenericStoredTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.{
|
|
||||||
GenericStoredTopologyTransactionsX,
|
|
||||||
PositiveStoredTopologyTransactionsX,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.store.TopologyTransactionRejection.Duplicate
|
|
||||||
import com.digitalasset.canton.topology.store.ValidatedTopologyTransactionX.GenericValidatedTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.store.db.DbTopologyStoreX
|
|
||||||
import com.digitalasset.canton.topology.store.memory.InMemoryTopologyStoreX
|
|
||||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyTransactionX.{
|
|
||||||
GenericTopologyTransactionX,
|
|
||||||
TxHash,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.transaction.*
|
|
||||||
import com.digitalasset.canton.tracing.TraceContext
|
|
||||||
import com.digitalasset.canton.version.ProtocolVersion
|
|
||||||
import com.google.common.annotations.VisibleForTesting
|
|
||||||
|
|
||||||
import scala.concurrent.duration.Duration
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
import scala.reflect.ClassTag
|
|
||||||
|
|
||||||
final case class StoredTopologyTransactionX[+Op <: TopologyChangeOpX, +M <: TopologyMappingX](
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
validFrom: EffectiveTime,
|
|
||||||
validUntil: Option[EffectiveTime],
|
|
||||||
transaction: SignedTopologyTransactionX[Op, M],
|
|
||||||
) extends DelegatedTopologyTransactionLike[Op, M]
|
|
||||||
with PrettyPrinting {
|
|
||||||
override protected def transactionLikeDelegate: TopologyTransactionLike[Op, M] = transaction
|
|
||||||
|
|
||||||
override def pretty: Pretty[StoredTopologyTransactionX.this.type] =
|
|
||||||
prettyOfClass(
|
|
||||||
unnamedParam(_.transaction),
|
|
||||||
param("sequenced", _.sequenced.value),
|
|
||||||
param("validFrom", _.validFrom.value),
|
|
||||||
paramIfDefined("validUntil", _.validUntil.map(_.value)),
|
|
||||||
)
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
|
||||||
def selectMapping[TargetMapping <: TopologyMappingX: ClassTag] = transaction
|
|
||||||
.selectMapping[TargetMapping]
|
|
||||||
.map(_ => this.asInstanceOf[StoredTopologyTransactionX[Op, TargetMapping]])
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
|
||||||
def selectOp[TargetOp <: TopologyChangeOpX: ClassTag] = transaction
|
|
||||||
.selectOp[TargetOp]
|
|
||||||
.map(_ => this.asInstanceOf[StoredTopologyTransactionX[TargetOp, M]])
|
|
||||||
}
|
|
||||||
|
|
||||||
object StoredTopologyTransactionX {
|
|
||||||
type GenericStoredTopologyTransactionX =
|
|
||||||
StoredTopologyTransactionX[TopologyChangeOpX, TopologyMappingX]
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ValidatedTopologyTransactionX[+Op <: TopologyChangeOpX, +M <: TopologyMappingX](
|
|
||||||
transaction: SignedTopologyTransactionX[Op, M],
|
|
||||||
rejectionReason: Option[TopologyTransactionRejection] = None,
|
|
||||||
expireImmediately: Boolean = false,
|
|
||||||
) extends DelegatedTopologyTransactionLike[Op, M]
|
|
||||||
with PrettyPrinting {
|
|
||||||
|
|
||||||
override protected def transactionLikeDelegate: TopologyTransactionLike[Op, M] = transaction
|
|
||||||
|
|
||||||
def nonDuplicateRejectionReason: Option[TopologyTransactionRejection] = rejectionReason match {
|
|
||||||
case Some(Duplicate(_)) => None
|
|
||||||
case otherwise => otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
def collectOfMapping[TargetM <: TopologyMappingX: ClassTag]
|
|
||||||
: Option[ValidatedTopologyTransactionX[Op, TargetM]] =
|
|
||||||
transaction.selectMapping[TargetM].map(tx => copy[Op, TargetM](transaction = tx))
|
|
||||||
|
|
||||||
def collectOf[TargetO <: TopologyChangeOpX: ClassTag, TargetM <: TopologyMappingX: ClassTag]
|
|
||||||
: Option[ValidatedTopologyTransactionX[TargetO, TargetM]] =
|
|
||||||
transaction.select[TargetO, TargetM].map(tx => copy[TargetO, TargetM](transaction = tx))
|
|
||||||
|
|
||||||
override def pretty: Pretty[ValidatedTopologyTransactionX.this.type] =
|
|
||||||
prettyOfClass(
|
|
||||||
unnamedParam(_.transaction),
|
|
||||||
paramIfDefined("rejectionReason", _.rejectionReason),
|
|
||||||
paramIfTrue("expireImmediately", _.expireImmediately),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
object ValidatedTopologyTransactionX {
|
|
||||||
type GenericValidatedTopologyTransactionX =
|
|
||||||
ValidatedTopologyTransactionX[TopologyChangeOpX, TopologyMappingX]
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class TopologyStoreX[+StoreID <: TopologyStoreId](implicit
|
|
||||||
protected val ec: ExecutionContext
|
|
||||||
) extends FlagCloseable {
|
|
||||||
this: NamedLogging =>
|
|
||||||
|
|
||||||
def storeId: 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 "pokes" of the
|
|
||||||
* topology client and updates of the acs commitment processor on startup
|
|
||||||
*/
|
|
||||||
def findUpcomingEffectiveChanges(asOfInclusive: CantonTimestamp)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[TopologyStoreX.Change]]
|
|
||||||
|
|
||||||
def maxTimestamp()(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[(SequencedTime, EffectiveTime)]]
|
|
||||||
|
|
||||||
/** returns the current dispatching watermark
|
|
||||||
*
|
|
||||||
* for topology transaction dispatching, we keep track up to which point in time
|
|
||||||
* we have mirrored the authorized store to the remote store
|
|
||||||
*
|
|
||||||
* the timestamp always refers to the timestamp of the authorized store!
|
|
||||||
*/
|
|
||||||
def currentDispatchingWatermark(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[CantonTimestamp]]
|
|
||||||
|
|
||||||
/** update the dispatching watermark for this target store */
|
|
||||||
def updateDispatchingWatermark(timestamp: CantonTimestamp)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Unit]
|
|
||||||
|
|
||||||
def findTransactionsByTxHash(asOfExclusive: EffectiveTime, hashes: Set[TxHash])(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]]
|
|
||||||
|
|
||||||
def findProposalsByTxHash(asOfExclusive: EffectiveTime, hashes: NonEmpty[Set[TxHash]])(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]]
|
|
||||||
|
|
||||||
def findTransactionsForMapping(asOfExclusive: EffectiveTime, hashes: NonEmpty[Set[MappingHash]])(
|
|
||||||
implicit traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]]
|
|
||||||
|
|
||||||
/** returns the set of positive transactions
|
|
||||||
*
|
|
||||||
* this function is used by the topology processor to determine the set of transaction, such that
|
|
||||||
* we can perform cascading updates if there was a certificate revocation
|
|
||||||
*
|
|
||||||
* @param asOfInclusive whether the search interval should include the current timepoint or not. the state at t is
|
|
||||||
* defined as "exclusive" of t, whereas for updating the state, we need to be able to query inclusive.
|
|
||||||
*/
|
|
||||||
def findPositiveTransactions(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
isProposal: Boolean,
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
filterUid: Option[Seq[UniqueIdentifier]],
|
|
||||||
filterNamespace: Option[Seq[Namespace]],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[PositiveStoredTopologyTransactionsX]
|
|
||||||
|
|
||||||
/** add validated topology transaction as is to the topology transaction table */
|
|
||||||
def update(
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
effective: EffectiveTime,
|
|
||||||
removeMapping: Map[MappingHash, PositiveInt],
|
|
||||||
removeTxs: Set[TxHash],
|
|
||||||
additions: Seq[GenericValidatedTopologyTransactionX],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Unit]
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
protected[topology] def dumpStoreContent()(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX]
|
|
||||||
|
|
||||||
/** store an initial set of topology transactions as given into the store */
|
|
||||||
def bootstrap(snapshot: GenericStoredTopologyTransactionsX)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Unit]
|
|
||||||
|
|
||||||
/** query optimized for inspection
|
|
||||||
*
|
|
||||||
* @param proposals if true, query only for proposals instead of approved transaction mappings
|
|
||||||
* @param recentTimestampO if exists, use this timestamp for the head state to prevent race conditions on the console
|
|
||||||
*/
|
|
||||||
def inspect(
|
|
||||||
proposals: Boolean,
|
|
||||||
timeQuery: TimeQuery,
|
|
||||||
// TODO(#14048) - consider removing `recentTimestampO` and moving callers to TimeQueryX.Snapshot
|
|
||||||
recentTimestampO: Option[CantonTimestamp],
|
|
||||||
op: Option[TopologyChangeOpX],
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
idFilter: Option[String],
|
|
||||||
namespaceFilter: Option[String],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]]
|
|
||||||
|
|
||||||
def inspectKnownParties(
|
|
||||||
timestamp: CantonTimestamp,
|
|
||||||
filterParty: String,
|
|
||||||
filterParticipant: String,
|
|
||||||
limit: Int,
|
|
||||||
)(implicit traceContext: TraceContext): Future[Set[PartyId]]
|
|
||||||
|
|
||||||
/** Finds the topology transaction that first onboarded the sequencer with ID `sequencerId`
|
|
||||||
*/
|
|
||||||
def findFirstSequencerStateForSequencer(
|
|
||||||
sequencerId: SequencerId
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, SequencerDomainStateX]]]
|
|
||||||
|
|
||||||
/** Finds the topology transaction that first onboarded the mediator with ID `mediatorId`
|
|
||||||
*/
|
|
||||||
def findFirstMediatorStateForMediator(
|
|
||||||
mediatorId: MediatorId
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, MediatorDomainStateX]]]
|
|
||||||
|
|
||||||
/** Finds the topology transaction that first onboarded the participant with ID `participantId`
|
|
||||||
*/
|
|
||||||
def findFirstTrustCertificateForParticipant(
|
|
||||||
participant: ParticipantId
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, DomainTrustCertificateX]]]
|
|
||||||
|
|
||||||
def findEssentialStateAtSequencedTime(
|
|
||||||
asOfInclusive: SequencedTime
|
|
||||||
)(implicit traceContext: TraceContext): Future[GenericStoredTopologyTransactionsX]
|
|
||||||
|
|
||||||
protected def signedTxFromStoredTx(
|
|
||||||
storedTx: GenericStoredTopologyTransactionX
|
|
||||||
): SignedTopologyTransactionX[TopologyChangeOpX, TopologyMappingX] = storedTx.transaction
|
|
||||||
|
|
||||||
def providesAdditionalSignatures(
|
|
||||||
transaction: GenericSignedTopologyTransactionX
|
|
||||||
)(implicit traceContext: TraceContext): Future[Boolean] = {
|
|
||||||
findStored(CantonTimestamp.MaxValue, transaction).map(_.forall { inStore =>
|
|
||||||
// check whether source still could provide an additional signature
|
|
||||||
transaction.signatures.diff(inStore.transaction.signatures.forgetNE).nonEmpty &&
|
|
||||||
// but only if the transaction in the target store is a valid proposal
|
|
||||||
inStore.transaction.isProposal &&
|
|
||||||
inStore.validUntil.isEmpty
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** returns initial set of onboarding transactions that should be dispatched to the domain */
|
|
||||||
def findParticipantOnboardingTransactions(participantId: ParticipantId, domainId: DomainId)(
|
|
||||||
implicit traceContext: TraceContext
|
|
||||||
): FutureUnlessShutdown[Seq[GenericSignedTopologyTransactionX]]
|
|
||||||
|
|
||||||
def findDispatchingTransactionsAfter(
|
|
||||||
timestampExclusive: CantonTimestamp,
|
|
||||||
limit: Option[Int],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX]
|
|
||||||
|
|
||||||
def findStoredForVersion(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericTopologyTransactionX,
|
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[GenericStoredTopologyTransactionX]]
|
|
||||||
|
|
||||||
final def exists(transaction: GenericSignedTopologyTransactionX)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Boolean] = findStored(CantonTimestamp.MaxValue, transaction).map(
|
|
||||||
_.exists(signedTxFromStoredTx(_) == transaction)
|
|
||||||
)
|
|
||||||
|
|
||||||
def findStored(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericSignedTopologyTransactionX,
|
|
||||||
includeRejected: Boolean = false,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[GenericStoredTopologyTransactionX]]
|
|
||||||
}
|
|
||||||
|
|
||||||
object TopologyStoreX {
|
|
||||||
|
|
||||||
sealed trait Change extends Product with Serializable {
|
|
||||||
def sequenced: SequencedTime
|
|
||||||
def effective: EffectiveTime
|
|
||||||
}
|
|
||||||
|
|
||||||
object Change {
|
|
||||||
final case class TopologyDelay(
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
effective: EffectiveTime,
|
|
||||||
epsilon: NonNegativeFiniteDuration,
|
|
||||||
) extends Change
|
|
||||||
|
|
||||||
final case class Other(sequenced: SequencedTime, effective: EffectiveTime) extends Change
|
|
||||||
}
|
|
||||||
|
|
||||||
def accumulateUpcomingEffectiveChanges(
|
|
||||||
items: Seq[StoredTopologyTransactionX[TopologyChangeOpX, TopologyMappingX]]
|
|
||||||
): Seq[Change] = {
|
|
||||||
items
|
|
||||||
.map(x => (x, x.mapping))
|
|
||||||
.map {
|
|
||||||
case (tx, x: DomainParametersStateX) =>
|
|
||||||
Change.TopologyDelay(tx.sequenced, tx.validFrom, x.parameters.topologyChangeDelay)
|
|
||||||
case (tx, _) => Change.Other(tx.sequenced, tx.validFrom)
|
|
||||||
}
|
|
||||||
.sortBy(_.effective)
|
|
||||||
.distinct
|
|
||||||
}
|
|
||||||
|
|
||||||
def apply[StoreID <: TopologyStoreId](
|
|
||||||
storeId: StoreID,
|
|
||||||
storage: Storage,
|
|
||||||
timeouts: ProcessingTimeout,
|
|
||||||
loggerFactory: NamedLoggerFactory,
|
|
||||||
)(implicit
|
|
||||||
ec: ExecutionContext
|
|
||||||
): TopologyStoreX[StoreID] = {
|
|
||||||
val storeLoggerFactory = loggerFactory.append("store", storeId.toString)
|
|
||||||
storage match {
|
|
||||||
case _: MemoryStorage =>
|
|
||||||
new InMemoryTopologyStoreX(storeId, storeLoggerFactory, timeouts)
|
|
||||||
case dbStorage: DbStorage =>
|
|
||||||
new DbTopologyStoreX(dbStorage, storeId, timeouts, storeLoggerFactory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy val initialParticipantDispatchingSet = Set(
|
|
||||||
TopologyMappingX.Code.DomainTrustCertificateX,
|
|
||||||
TopologyMappingX.Code.OwnerToKeyMappingX,
|
|
||||||
// TODO(#14060) - potentially revisit this once we implement TopologyStoreX.filterInitialParticipantDispatchingTransactions
|
|
||||||
TopologyMappingX.Code.NamespaceDelegationX,
|
|
||||||
TopologyMappingX.Code.IdentifierDelegationX,
|
|
||||||
TopologyMappingX.Code.DecentralizedNamespaceDefinitionX,
|
|
||||||
)
|
|
||||||
|
|
||||||
def filterInitialParticipantDispatchingTransactions(
|
|
||||||
participantId: ParticipantId,
|
|
||||||
domainId: DomainId,
|
|
||||||
transactions: Seq[GenericStoredTopologyTransactionX],
|
|
||||||
): Seq[GenericSignedTopologyTransactionX] = {
|
|
||||||
// TODO(#14060): Extend filtering along the lines of:
|
|
||||||
// TopologyStore.filterInitialParticipantDispatchingTransactions
|
|
||||||
transactions.map(_.transaction).collect {
|
|
||||||
case tx @ SignedTopologyTransactionX(
|
|
||||||
TopologyTransactionX(_, _, DomainTrustCertificateX(`participantId`, `domainId`, _, _)),
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
) =>
|
|
||||||
tx
|
|
||||||
case tx @ SignedTopologyTransactionX(
|
|
||||||
TopologyTransactionX(_, _, OwnerToKeyMappingX(`participantId`, _, _)),
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
) =>
|
|
||||||
tx
|
|
||||||
case tx @ SignedTopologyTransactionX(
|
|
||||||
TopologyTransactionX(_, _, NamespaceDelegationX(ns, _, _)),
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
) if ns == participantId.uid.namespace =>
|
|
||||||
tx
|
|
||||||
case tx @ SignedTopologyTransactionX(
|
|
||||||
TopologyTransactionX(_, _, IdentifierDelegationX(uid, _)),
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
) if uid == participantId.uid =>
|
|
||||||
tx
|
|
||||||
case tx @ SignedTopologyTransactionX(
|
|
||||||
TopologyTransactionX(_, _, _: DecentralizedNamespaceDefinitionX),
|
|
||||||
_,
|
|
||||||
_,
|
|
||||||
) =>
|
|
||||||
tx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** convenience method waiting until the last eligible transaction inserted into the source store has been dispatched successfully to the target domain */
|
|
||||||
def awaitTxObserved(
|
|
||||||
client: DomainTopologyClient,
|
|
||||||
transaction: GenericSignedTopologyTransactionX,
|
|
||||||
target: TopologyStoreX[?],
|
|
||||||
timeout: Duration,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext,
|
|
||||||
executionContext: ExecutionContext,
|
|
||||||
): FutureUnlessShutdown[Boolean] = {
|
|
||||||
client.await(
|
|
||||||
// we know that the transaction is stored and effective once we find it in the target
|
|
||||||
// domain store and once the effective time (valid from) is smaller than the client timestamp
|
|
||||||
sp => target.findStored(sp.timestamp, transaction, includeRejected = true).map(_.nonEmpty),
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed trait TimeQuery {
|
|
||||||
def toProtoV30: topoV30.BaseQuery.TimeQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
object TimeQuery {
|
|
||||||
object HeadState extends TimeQuery {
|
|
||||||
override def toProtoV30: topoV30.BaseQuery.TimeQuery =
|
|
||||||
topoV30.BaseQuery.TimeQuery.HeadState(com.google.protobuf.empty.Empty())
|
|
||||||
}
|
|
||||||
final case class Snapshot(asOf: CantonTimestamp) extends TimeQuery {
|
|
||||||
override def toProtoV30: topoV30.BaseQuery.TimeQuery =
|
|
||||||
topoV30.BaseQuery.TimeQuery.Snapshot(asOf.toProtoTimestamp)
|
|
||||||
}
|
|
||||||
final case class Range(from: Option[CantonTimestamp], until: Option[CantonTimestamp])
|
|
||||||
extends TimeQuery {
|
|
||||||
override def toProtoV30: topoV30.BaseQuery.TimeQuery = topoV30.BaseQuery.TimeQuery.Range(
|
|
||||||
topoV30.BaseQuery.TimeRange(from.map(_.toProtoTimestamp), until.map(_.toProtoTimestamp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def fromProto(
|
|
||||||
proto: topoV30.BaseQuery.TimeQuery,
|
|
||||||
fieldName: String,
|
|
||||||
): ParsingResult[TimeQuery] =
|
|
||||||
proto match {
|
|
||||||
case topoV30.BaseQuery.TimeQuery.Empty =>
|
|
||||||
Left(ProtoDeserializationError.FieldNotSet(fieldName))
|
|
||||||
case topoV30.BaseQuery.TimeQuery.Snapshot(value) =>
|
|
||||||
CantonTimestamp.fromProtoTimestamp(value).map(Snapshot)
|
|
||||||
case topoV30.BaseQuery.TimeQuery.HeadState(_) => Right(HeadState)
|
|
||||||
case topoV30.BaseQuery.TimeQuery.Range(value) =>
|
|
||||||
for {
|
|
||||||
fromO <- value.from.traverse(CantonTimestamp.fromProtoTimestamp)
|
|
||||||
toO <- value.until.traverse(CantonTimestamp.fromProtoTimestamp)
|
|
||||||
} yield Range(fromO, toO)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package com.digitalasset.canton.topology.store
|
||||||
|
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.String256M
|
||||||
|
import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong}
|
||||||
|
import com.digitalasset.canton.crypto.{Fingerprint, SignatureCheckError}
|
||||||
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
|
import com.digitalasset.canton.logging.ErrorLoggingContext
|
||||||
|
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||||
|
import com.digitalasset.canton.protocol.OnboardingRestriction
|
||||||
|
import com.digitalasset.canton.topology.{
|
||||||
|
DomainId,
|
||||||
|
Member,
|
||||||
|
ParticipantId,
|
||||||
|
PartyId,
|
||||||
|
TopologyManagerError,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed trait TopologyTransactionRejection extends PrettyPrinting with Product with Serializable {
|
||||||
|
def asString: String
|
||||||
|
def asString1GB: String256M =
|
||||||
|
String256M.tryCreate(asString, Some("topology transaction rejection"))
|
||||||
|
|
||||||
|
def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError
|
||||||
|
}
|
||||||
|
object TopologyTransactionRejection {
|
||||||
|
|
||||||
|
final case class NoDelegationFoundForKeys(keys: Set[Fingerprint])
|
||||||
|
extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = s"No delegation found for keys ${keys.mkString(", ")}"
|
||||||
|
override def pretty: Pretty[NoDelegationFoundForKeys] = prettyOfString(_ => asString)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
||||||
|
TopologyManagerError.UnauthorizedTransaction.Failure(asString)
|
||||||
|
|
||||||
|
}
|
||||||
|
case object NotAuthorized extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = "Not authorized"
|
||||||
|
override def pretty: Pretty[NotAuthorized.type] = prettyOfString(_ => asString)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
||||||
|
TopologyManagerError.UnauthorizedTransaction.Failure(asString)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ThresholdTooHigh(actual: Int, mustBeAtMost: Int)
|
||||||
|
extends TopologyTransactionRejection {
|
||||||
|
override def asString: String =
|
||||||
|
s"Threshold must not be higher than $mustBeAtMost, but was $actual."
|
||||||
|
|
||||||
|
override def pretty: Pretty[ThresholdTooHigh] = prettyOfString(_ => asString)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) = {
|
||||||
|
TopologyManagerError.InvalidThreshold.ThresholdTooHigh(actual, mustBeAtMost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class OnboardingRestrictionInPlace(
|
||||||
|
participant: ParticipantId,
|
||||||
|
restriction: OnboardingRestriction,
|
||||||
|
loginAfter: Option[CantonTimestamp],
|
||||||
|
) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String =
|
||||||
|
s"Participant ${participant} onboarding rejected as restrictions ${restriction} are in place."
|
||||||
|
|
||||||
|
override def pretty: Pretty[OnboardingRestrictionInPlace] = prettyOfString(_ => asString)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) = {
|
||||||
|
TopologyManagerError.ParticipantOnboardingRefused.Reject(participant, restriction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class SignatureCheckFailed(err: SignatureCheckError)
|
||||||
|
extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = err.toString
|
||||||
|
override def pretty: Pretty[SignatureCheckFailed] = prettyOfClass(param("err", _.err))
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
||||||
|
TopologyManagerError.InvalidSignatureError.Failure(err)
|
||||||
|
}
|
||||||
|
final case class WrongDomain(wrong: DomainId) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = show"Wrong domain $wrong"
|
||||||
|
override def pretty: Pretty[WrongDomain] = prettyOfClass(param("wrong", _.wrong))
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
||||||
|
TopologyManagerError.WrongDomain.Failure(wrong)
|
||||||
|
}
|
||||||
|
final case class Duplicate(old: CantonTimestamp) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = show"Duplicate transaction from ${old}"
|
||||||
|
override def pretty: Pretty[Duplicate] = prettyOfClass(param("old", _.old))
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
||||||
|
TopologyManagerError.DuplicateTransaction.ExistsAt(old)
|
||||||
|
}
|
||||||
|
final case class SerialMismatch(expected: PositiveInt, actual: PositiveInt)
|
||||||
|
extends TopologyTransactionRejection {
|
||||||
|
override def asString: String =
|
||||||
|
show"The given serial $actual does not match the expected serial $expected"
|
||||||
|
override def pretty: Pretty[SerialMismatch] =
|
||||||
|
prettyOfClass(param("expected", _.expected), param("actual", _.actual))
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
||||||
|
TopologyManagerError.SerialMismatch.Failure(expected, actual)
|
||||||
|
}
|
||||||
|
final case class Other(str: String) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = str
|
||||||
|
override def pretty: Pretty[Other] = prettyOfString(_ => asString)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext) =
|
||||||
|
TopologyManagerError.InternalError.Other(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ExtraTrafficLimitTooLow(
|
||||||
|
member: Member,
|
||||||
|
actual: PositiveLong,
|
||||||
|
expectedMinimum: PositiveLong,
|
||||||
|
) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String =
|
||||||
|
s"Extra traffic limit for $member should be at least $expectedMinimum, but was $actual."
|
||||||
|
|
||||||
|
override def pretty: Pretty[ExtraTrafficLimitTooLow] =
|
||||||
|
prettyOfClass(
|
||||||
|
param("member", _.member),
|
||||||
|
param("actual", _.actual),
|
||||||
|
param("expectedMinimum", _.expectedMinimum),
|
||||||
|
)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
||||||
|
TopologyManagerError.InvalidTrafficLimit.TrafficLimitTooLow(member, actual, expectedMinimum)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class InsufficientKeys(members: Seq[Member]) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String =
|
||||||
|
s"Members ${members.sorted.mkString(", ")} are missing a signing key or an encryption key or both."
|
||||||
|
|
||||||
|
override def pretty: Pretty[InsufficientKeys] = prettyOfClass(
|
||||||
|
param("members", _.members)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
||||||
|
TopologyManagerError.InsufficientKeys.Failure(members)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class UnknownMembers(members: Seq[Member]) extends TopologyTransactionRejection {
|
||||||
|
override def asString: String = s"Members ${members.toSeq.sorted.mkString(", ")} are unknown."
|
||||||
|
|
||||||
|
override def pretty: Pretty[UnknownMembers] = prettyOfClass(param("members", _.members))
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
||||||
|
TopologyManagerError.UnknownMembers.Failure(members)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class ParticipantStillHostsParties(participantId: ParticipantId, parties: Seq[PartyId])
|
||||||
|
extends TopologyTransactionRejection {
|
||||||
|
override def asString: String =
|
||||||
|
s"Cannot remove domain trust certificate for $participantId because it still hosts parties ${parties
|
||||||
|
.mkString(",")}"
|
||||||
|
|
||||||
|
override def pretty: Pretty[ParticipantStillHostsParties] =
|
||||||
|
prettyOfClass(param("participantId", _.participantId), param("parties", _.parties))
|
||||||
|
|
||||||
|
override def toTopologyManagerError(implicit elc: ErrorLoggingContext): TopologyManagerError =
|
||||||
|
TopologyManagerError.IllegalRemovalOfDomainTrustCertificate.ParticipantStillHostsParties(
|
||||||
|
participantId,
|
||||||
|
parties,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package com.digitalasset.canton.topology.store.db
|
||||||
|
|
||||||
|
import com.daml.nameof.NameOf.functionFullName
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.LengthLimitedString.DisplayName
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.{String255, String300}
|
||||||
|
import com.digitalasset.canton.config.ProcessingTimeout
|
||||||
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
|
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||||
|
import com.digitalasset.canton.resource.DbStorage.{DbAction, SQLActionBuilderChain}
|
||||||
|
import com.digitalasset.canton.resource.{DbStorage, DbStore}
|
||||||
|
import com.digitalasset.canton.topology.store.{PartyMetadata, PartyMetadataStore}
|
||||||
|
import com.digitalasset.canton.topology.{ParticipantId, PartyId, UniqueIdentifier}
|
||||||
|
import com.digitalasset.canton.tracing.TraceContext
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class DbPartyMetadataStore(
|
||||||
|
override protected val storage: DbStorage,
|
||||||
|
override protected val timeouts: ProcessingTimeout,
|
||||||
|
override protected val loggerFactory: NamedLoggerFactory,
|
||||||
|
)(implicit
|
||||||
|
ec: ExecutionContext
|
||||||
|
) extends PartyMetadataStore
|
||||||
|
with DbStore {
|
||||||
|
|
||||||
|
import DbStorage.Implicits.BuilderChain.*
|
||||||
|
import storage.api.*
|
||||||
|
|
||||||
|
override def metadataForParty(
|
||||||
|
partyId: PartyId
|
||||||
|
)(implicit traceContext: TraceContext): Future[Option[PartyMetadata]] = {
|
||||||
|
storage
|
||||||
|
.query(
|
||||||
|
metadataForPartyQuery(sql"party_id = $partyId #${storage.limit(1)}"),
|
||||||
|
functionFullName,
|
||||||
|
)
|
||||||
|
.map(_.headOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def metadataForPartyQuery(
|
||||||
|
where: SQLActionBuilderChain
|
||||||
|
): DbAction.ReadOnly[Seq[PartyMetadata]] = {
|
||||||
|
|
||||||
|
val query =
|
||||||
|
sql"select party_id, display_name, participant_id, submission_id, effective_at, notified from common_party_metadata where " ++ where
|
||||||
|
|
||||||
|
for {
|
||||||
|
data <- query
|
||||||
|
.as[(PartyId, Option[String], Option[String], String255, CantonTimestamp, Boolean)]
|
||||||
|
} yield {
|
||||||
|
data.map {
|
||||||
|
case (partyId, displayNameS, participantIdS, submissionId, effectiveAt, notified) =>
|
||||||
|
val participantId =
|
||||||
|
participantIdS
|
||||||
|
.flatMap(x => UniqueIdentifier.fromProtoPrimitive_(x).toOption)
|
||||||
|
.map(ParticipantId(_))
|
||||||
|
val displayName = displayNameS.flatMap(String255.create(_).toOption)
|
||||||
|
PartyMetadata(
|
||||||
|
partyId,
|
||||||
|
displayName,
|
||||||
|
participantId = participantId,
|
||||||
|
)(
|
||||||
|
effectiveTimestamp = effectiveAt,
|
||||||
|
submissionId = submissionId,
|
||||||
|
notified = notified,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def insertOrUpdatePartyMetadata(
|
||||||
|
partyId: PartyId,
|
||||||
|
participantId: Option[ParticipantId],
|
||||||
|
displayName: Option[DisplayName],
|
||||||
|
effectiveTimestamp: CantonTimestamp,
|
||||||
|
submissionId: String255,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = {
|
||||||
|
val participantS = dbValue(participantId)
|
||||||
|
val query = storage.profile match {
|
||||||
|
case _: DbStorage.Profile.Postgres =>
|
||||||
|
sqlu"""insert into common_party_metadata (party_id, display_name, participant_id, submission_id, effective_at)
|
||||||
|
VALUES ($partyId, $displayName, $participantS, $submissionId, $effectiveTimestamp)
|
||||||
|
on conflict (party_id) do update
|
||||||
|
set
|
||||||
|
display_name = $displayName,
|
||||||
|
participant_id = $participantS,
|
||||||
|
submission_id = $submissionId,
|
||||||
|
effective_at = $effectiveTimestamp,
|
||||||
|
notified = false
|
||||||
|
"""
|
||||||
|
case _: DbStorage.Profile.H2 | _: DbStorage.Profile.Oracle =>
|
||||||
|
sqlu"""merge into common_party_metadata
|
||||||
|
using dual
|
||||||
|
on (party_id = $partyId)
|
||||||
|
when matched then
|
||||||
|
update set
|
||||||
|
display_name = $displayName,
|
||||||
|
participant_id = $participantS,
|
||||||
|
submission_id = $submissionId,
|
||||||
|
effective_at = $effectiveTimestamp,
|
||||||
|
notified = ${false}
|
||||||
|
when not matched then
|
||||||
|
insert (party_id, display_name, participant_id, submission_id, effective_at)
|
||||||
|
values ($partyId, $displayName, $participantS, $submissionId, $effectiveTimestamp)
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
storage.update_(query, functionFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def dbValue(participantId: Option[ParticipantId]): Option[String300] =
|
||||||
|
participantId.map(_.uid.toLengthLimitedString.asString300)
|
||||||
|
|
||||||
|
/** mark the given metadata has having been successfully forwarded to the domain */
|
||||||
|
override def markNotified(
|
||||||
|
metadata: PartyMetadata
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = {
|
||||||
|
val partyId = metadata.partyId
|
||||||
|
val effectiveAt = metadata.effectiveTimestamp
|
||||||
|
val query =
|
||||||
|
sqlu"UPDATE common_party_metadata SET notified = ${true} WHERE party_id = $partyId and effective_at = $effectiveAt"
|
||||||
|
storage.update_(query, functionFullName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** fetch the current set of party data which still needs to be notified */
|
||||||
|
override def fetchNotNotified()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[PartyMetadata]] = {
|
||||||
|
storage
|
||||||
|
.query(
|
||||||
|
metadataForPartyQuery(sql"notified = ${false}"),
|
||||||
|
functionFullName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -3,136 +3,868 @@
|
|||||||
|
|
||||||
package com.digitalasset.canton.topology.store.db
|
package com.digitalasset.canton.topology.store.db
|
||||||
|
|
||||||
|
import cats.syntax.option.*
|
||||||
import com.daml.nameof.NameOf.functionFullName
|
import com.daml.nameof.NameOf.functionFullName
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.LengthLimitedString.DisplayName
|
import com.daml.nonempty.NonEmpty
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.{String255, String300}
|
import com.digitalasset.canton.config.CantonRequireTypes.{LengthLimitedString, String185}
|
||||||
import com.digitalasset.canton.config.ProcessingTimeout
|
import com.digitalasset.canton.config.ProcessingTimeout
|
||||||
|
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
|
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
|
||||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||||
import com.digitalasset.canton.resource.DbStorage.{DbAction, SQLActionBuilderChain}
|
import com.digitalasset.canton.resource.DbStorage.SQLActionBuilderChain
|
||||||
import com.digitalasset.canton.resource.{DbStorage, DbStore}
|
import com.digitalasset.canton.resource.{DbStorage, DbStore}
|
||||||
import com.digitalasset.canton.topology.*
|
import com.digitalasset.canton.topology.*
|
||||||
|
import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime}
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionX.GenericStoredTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.{
|
||||||
|
GenericStoredTopologyTransactionsX,
|
||||||
|
PositiveStoredTopologyTransactionsX,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.store.ValidatedTopologyTransactionX.GenericValidatedTopologyTransactionX
|
||||||
import com.digitalasset.canton.topology.store.*
|
import com.digitalasset.canton.topology.store.*
|
||||||
|
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyChangeOpX.Replace
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyTransactionX.{
|
||||||
|
GenericTopologyTransactionX,
|
||||||
|
TxHash,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.transaction.*
|
||||||
import com.digitalasset.canton.tracing.TraceContext
|
import com.digitalasset.canton.tracing.TraceContext
|
||||||
|
import com.digitalasset.canton.util.FutureInstances.*
|
||||||
|
import com.digitalasset.canton.util.MonadUtil
|
||||||
|
import com.digitalasset.canton.version.ProtocolVersion
|
||||||
|
import com.google.common.annotations.VisibleForTesting
|
||||||
|
import slick.jdbc.canton.SQLActionBuilder
|
||||||
|
import slick.jdbc.{GetResult, TransactionIsolation}
|
||||||
|
import slick.sql.SqlStreamingAction
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
class DbPartyMetadataStore(
|
class DbTopologyStoreX[StoreId <: TopologyStoreId](
|
||||||
override protected val storage: DbStorage,
|
override protected val storage: DbStorage,
|
||||||
|
val storeId: StoreId,
|
||||||
override protected val timeouts: ProcessingTimeout,
|
override protected val timeouts: ProcessingTimeout,
|
||||||
override protected val loggerFactory: NamedLoggerFactory,
|
override protected val loggerFactory: NamedLoggerFactory,
|
||||||
)(implicit
|
protected val maxItemsInSqlQuery: PositiveInt = PositiveInt.tryCreate(100),
|
||||||
ec: ExecutionContext
|
)(implicit ec: ExecutionContext)
|
||||||
) extends PartyMetadataStore
|
extends TopologyStoreX[StoreId]
|
||||||
with DbStore {
|
with DbStore {
|
||||||
|
|
||||||
import DbStorage.Implicits.BuilderChain.*
|
import DbStorage.Implicits.BuilderChain.*
|
||||||
import storage.api.*
|
import storage.api.*
|
||||||
|
import storage.converters.*
|
||||||
|
|
||||||
override def metadataForParty(
|
private implicit val getResultSignedTopologyTransaction
|
||||||
partyId: PartyId
|
: GetResult[GenericSignedTopologyTransactionX] =
|
||||||
)(implicit traceContext: TraceContext): Future[Option[PartyMetadata]] = {
|
SignedTopologyTransactionX.createGetResultDomainTopologyTransaction
|
||||||
storage
|
|
||||||
.query(
|
protected val transactionStoreIdName: LengthLimitedString = storeId.dbString
|
||||||
metadataForPartyQuery(sql"party_id = $partyId #${storage.limit(1)}"),
|
|
||||||
functionFullName,
|
def findTransactionsByTxHash(asOfExclusive: EffectiveTime, hashes: Set[TxHash])(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
|
||||||
|
if (hashes.isEmpty) Future.successful(Seq.empty)
|
||||||
|
else {
|
||||||
|
logger.debug(s"Querying transactions for tx hashes $hashes as of $asOfExclusive")
|
||||||
|
|
||||||
|
findAsOfExclusive(
|
||||||
|
asOfExclusive,
|
||||||
|
sql" AND (" ++ hashes
|
||||||
|
.map(txHash => sql"tx_hash = ${txHash.hash.toLengthLimitedHexString}")
|
||||||
|
.toList
|
||||||
|
.intercalate(sql" OR ") ++ sql")",
|
||||||
|
operation = "transactionsByTxHash",
|
||||||
)
|
)
|
||||||
.map(_.headOption)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def metadataForPartyQuery(
|
override def findProposalsByTxHash(
|
||||||
where: SQLActionBuilderChain
|
asOfExclusive: EffectiveTime,
|
||||||
): DbAction.ReadOnly[Seq[PartyMetadata]] = {
|
hashes: NonEmpty[Set[TxHash]],
|
||||||
|
)(implicit traceContext: TraceContext): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
logger.debug(s"Querying proposals for tx hashes $hashes as of $asOfExclusive")
|
||||||
|
|
||||||
|
findAsOfExclusive(
|
||||||
|
asOfExclusive,
|
||||||
|
sql" AND is_proposal = true AND (" ++ hashes
|
||||||
|
.map(txHash => sql"tx_hash = ${txHash.hash.toLengthLimitedHexString}")
|
||||||
|
.forgetNE
|
||||||
|
.toList
|
||||||
|
.intercalate(sql" OR ") ++ sql")",
|
||||||
|
"proposalsByTxHash",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findTransactionsForMapping(
|
||||||
|
asOfExclusive: EffectiveTime,
|
||||||
|
hashes: NonEmpty[Set[MappingHash]],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
logger.debug(s"Querying proposals for mapping hashes $hashes as of $asOfExclusive")
|
||||||
|
|
||||||
|
findAsOfExclusive(
|
||||||
|
asOfExclusive,
|
||||||
|
sql" AND is_proposal = false AND (" ++ hashes
|
||||||
|
.map(mappingHash => sql"mapping_key_hash = ${mappingHash.hash.toLengthLimitedHexString}")
|
||||||
|
.forgetNE
|
||||||
|
.toList
|
||||||
|
.intercalate(sql" OR ") ++ sql")",
|
||||||
|
operation = "transactionsForMapping",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param elements Elements to be batched
|
||||||
|
* @param operationName Name of the operation
|
||||||
|
* @param f Create a DBIOAction from a batch
|
||||||
|
*/
|
||||||
|
private def performBatchedDbOperation[X](
|
||||||
|
elements: Seq[X],
|
||||||
|
operationName: String,
|
||||||
|
processInParallel: Boolean,
|
||||||
|
)(
|
||||||
|
f: Seq[X] => DBIOAction[_, NoStream, Effect.Write with Effect.Transactional]
|
||||||
|
)(implicit traceContext: TraceContext) = if (elements.isEmpty) Future.successful(())
|
||||||
|
else
|
||||||
|
MonadUtil.batchedSequentialTraverse_(
|
||||||
|
parallelism =
|
||||||
|
if (processInParallel) PositiveInt.two * storage.threadsAvailableForWriting
|
||||||
|
else PositiveInt.one,
|
||||||
|
chunkSize = maxItemsInSqlQuery,
|
||||||
|
)(elements) { elementsBatch =>
|
||||||
|
storage.update_(
|
||||||
|
f(elementsBatch),
|
||||||
|
operationName = operationName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** add validated topology transaction as is to the topology transaction table
|
||||||
|
*/
|
||||||
|
override def update(
|
||||||
|
sequenced: SequencedTime,
|
||||||
|
effective: EffectiveTime,
|
||||||
|
removeMapping: Map[TopologyMappingX.MappingHash, PositiveInt],
|
||||||
|
removeTxs: Set[TopologyTransactionX.TxHash],
|
||||||
|
additions: Seq[GenericValidatedTopologyTransactionX],
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = {
|
||||||
|
|
||||||
|
val effectiveTs = effective.value
|
||||||
|
|
||||||
|
val transactionRemovals = removeMapping.toList.map { case (mappingHash, serial) =>
|
||||||
|
sql"mapping_key_hash=${mappingHash.hash.toLengthLimitedHexString} and serial_counter <= $serial"
|
||||||
|
} ++ removeTxs.map(txHash => sql"tx_hash=${txHash.hash.toLengthLimitedHexString}")
|
||||||
|
|
||||||
|
lazy val updateRemovals =
|
||||||
|
(sql"UPDATE common_topology_transactions SET valid_until = ${Some(effectiveTs)} WHERE store_id=$transactionStoreIdName AND (" ++
|
||||||
|
transactionRemovals
|
||||||
|
.intercalate(
|
||||||
|
sql" OR "
|
||||||
|
) ++ sql") AND valid_from < $effectiveTs AND valid_until is null").asUpdate
|
||||||
|
|
||||||
|
lazy val insertAdditions =
|
||||||
|
insertSignedTransaction[GenericValidatedTopologyTransactionX](vtx =>
|
||||||
|
TransactionEntry(
|
||||||
|
sequenced,
|
||||||
|
effective,
|
||||||
|
Option.when(
|
||||||
|
vtx.rejectionReason.nonEmpty || vtx.expireImmediately
|
||||||
|
)(effective),
|
||||||
|
vtx.transaction,
|
||||||
|
vtx.rejectionReason,
|
||||||
|
)
|
||||||
|
)(additions)
|
||||||
|
|
||||||
|
storage.update_(
|
||||||
|
DBIO
|
||||||
|
.seq(
|
||||||
|
if (transactionRemovals.nonEmpty) updateRemovals else DBIO.successful(0),
|
||||||
|
if (additions.nonEmpty) insertAdditions
|
||||||
|
else DBIO.successful(0),
|
||||||
|
)
|
||||||
|
.transactionally
|
||||||
|
.withTransactionIsolation(TransactionIsolation.Serializable),
|
||||||
|
operationName = "update-topology-transactions",
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
override protected[topology] def dumpStoreContent()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
// Helper case class to produce comparable output to the InMemoryStore
|
||||||
|
case class TopologyStoreEntry(
|
||||||
|
transaction: GenericSignedTopologyTransactionX,
|
||||||
|
sequenced: SequencedTime,
|
||||||
|
from: EffectiveTime,
|
||||||
|
until: Option[EffectiveTime],
|
||||||
|
rejected: Option[String],
|
||||||
|
)
|
||||||
|
|
||||||
val query =
|
val query =
|
||||||
sql"select party_id, display_name, participant_id, submission_id, effective_at, notified from common_party_metadata where " ++ where
|
sql"SELECT instance, sequenced, valid_from, valid_until, rejection_reason FROM common_topology_transactions WHERE store_id = $transactionStoreIdName ORDER BY id"
|
||||||
|
|
||||||
|
val entriesF =
|
||||||
|
storage
|
||||||
|
.query(
|
||||||
|
query.as[
|
||||||
|
(
|
||||||
|
GenericSignedTopologyTransactionX,
|
||||||
|
CantonTimestamp,
|
||||||
|
CantonTimestamp,
|
||||||
|
Option[CantonTimestamp],
|
||||||
|
Option[String],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
functionFullName,
|
||||||
|
)
|
||||||
|
.map(_.map { case (tx, sequencedTs, validFrom, validUntil, rejectionReason) =>
|
||||||
|
TopologyStoreEntry(
|
||||||
|
tx,
|
||||||
|
SequencedTime(sequencedTs),
|
||||||
|
EffectiveTime(validFrom),
|
||||||
|
validUntil.map(EffectiveTime(_)),
|
||||||
|
rejectionReason,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
entriesF.map { entries =>
|
||||||
|
logger.debug(
|
||||||
|
entries
|
||||||
|
.map(_.toString)
|
||||||
|
.mkString("Topology Store Content[", ", ", "]")
|
||||||
|
)
|
||||||
|
StoredTopologyTransactionsX(
|
||||||
|
entries.map(e => StoredTopologyTransactionX(e.sequenced, e.from, e.until, e.transaction))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def inspect(
|
||||||
|
proposals: Boolean,
|
||||||
|
timeQuery: TimeQuery,
|
||||||
|
recentTimestampO: Option[CantonTimestamp],
|
||||||
|
op: Option[TopologyChangeOpX],
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
idFilter: Option[String],
|
||||||
|
namespaceFilter: Option[String],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]] = {
|
||||||
|
logger.debug(s"Inspecting store for types=$types, filter=$idFilter, time=$timeQuery")
|
||||||
|
|
||||||
|
val timeFilter: SQLActionBuilderChain = timeQuery match {
|
||||||
|
case TimeQuery.HeadState =>
|
||||||
|
getHeadStateQuery(recentTimestampO)
|
||||||
|
case TimeQuery.Snapshot(asOf) =>
|
||||||
|
asOfQuery(asOf = asOf, asOfInclusive = false)
|
||||||
|
case TimeQuery.Range(None, None) =>
|
||||||
|
sql"" // The case below inserts an additional `AND` that we don't want
|
||||||
|
case TimeQuery.Range(from, until) =>
|
||||||
|
sql" AND " ++ ((from.toList.map(ts => sql"valid_from >= $ts") ++ until.toList.map(ts =>
|
||||||
|
sql"valid_from <= $ts"
|
||||||
|
))
|
||||||
|
.intercalate(sql" AND "))
|
||||||
|
}
|
||||||
|
|
||||||
|
val operationFilter = op.map(value => sql" AND operation = $value").getOrElse(sql"")
|
||||||
|
|
||||||
|
val mappingIdFilter = getIdFilter(idFilter)
|
||||||
|
val mappingNameSpaceFilter = getNamespaceFilter(namespaceFilter)
|
||||||
|
|
||||||
|
val mappingTypeFilter = typeFilter(types.toSet)
|
||||||
|
|
||||||
|
val mappingProposalsAndPreviousFilter =
|
||||||
|
timeFilter ++ operationFilter ++ mappingIdFilter ++ mappingNameSpaceFilter ++ mappingTypeFilter ++ sql" AND is_proposal = $proposals"
|
||||||
|
|
||||||
|
queryForTransactions(mappingProposalsAndPreviousFilter, "inspect")
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings(Array("com.digitalasset.canton.SlickString"))
|
||||||
|
override def inspectKnownParties(
|
||||||
|
timestamp: CantonTimestamp,
|
||||||
|
filterParty: String,
|
||||||
|
filterParticipant: String,
|
||||||
|
limit: Int,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Set[PartyId]] = {
|
||||||
|
logger.debug(
|
||||||
|
s"Inspecting known parties at t=$timestamp with filterParty=$filterParty and filterParticipant=$filterParticipant"
|
||||||
|
)
|
||||||
|
|
||||||
|
def splitFilterPrefixAndSql(uidFilter: String): (String, String, String, String) =
|
||||||
|
UniqueIdentifier.splitFilter(uidFilter) match {
|
||||||
|
case (id, ns) => (id, ns, id + "%", ns + "%")
|
||||||
|
}
|
||||||
|
|
||||||
|
val (prefixPartyIdentifier, prefixPartyNS, sqlPartyIdentifier, sqlPartyNS) =
|
||||||
|
splitFilterPrefixAndSql(filterParty)
|
||||||
|
val (
|
||||||
|
prefixParticipantIdentifier,
|
||||||
|
prefixParticipantNS,
|
||||||
|
sqlParticipantIdentifier,
|
||||||
|
sqlParticipantNS,
|
||||||
|
) =
|
||||||
|
splitFilterPrefixAndSql(filterParticipant)
|
||||||
|
|
||||||
|
// conditional append avoids "like '%'" filters on empty filters
|
||||||
|
def conditionalAppend(filter: String, sqlIdentifier: String, sqlNamespace: String) =
|
||||||
|
if (filter.nonEmpty)
|
||||||
|
sql" AND identifier LIKE ${sqlIdentifier} AND namespace LIKE ${sqlNamespace}"
|
||||||
|
else sql""
|
||||||
|
|
||||||
|
queryForTransactions(
|
||||||
|
asOfQuery(timestamp, asOfInclusive = false) ++
|
||||||
|
sql" AND NOT is_proposal AND operation = ${TopologyChangeOpX.Replace} AND ("
|
||||||
|
// PartyToParticipantX filtering
|
||||||
|
++ Seq(
|
||||||
|
sql"(transaction_type = ${PartyToParticipantX.code}"
|
||||||
|
++ conditionalAppend(filterParty, sqlPartyIdentifier, sqlPartyNS)
|
||||||
|
++ sql")"
|
||||||
|
)
|
||||||
|
++ sql" OR "
|
||||||
|
// DomainTrustCertificateX filtering
|
||||||
|
++ Seq(
|
||||||
|
sql"(transaction_type = ${DomainTrustCertificateX.code}"
|
||||||
|
// In DomainTrustCertificateX part of the filter, compare not only to participant, but also to party identifier
|
||||||
|
// to enable searching for the admin party
|
||||||
|
++ conditionalAppend(filterParty, sqlPartyIdentifier, sqlPartyNS)
|
||||||
|
++ conditionalAppend(filterParticipant, sqlParticipantIdentifier, sqlParticipantNS)
|
||||||
|
++ sql")"
|
||||||
|
)
|
||||||
|
++ sql")",
|
||||||
|
storage.limit(limit),
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
_.result.toSet
|
||||||
|
.flatMap[PartyId](_.mapping match {
|
||||||
|
// TODO(#14061): post-filtering for participantId non-columns results in fewer than limit results being returned
|
||||||
|
// - add indexed secondary uid and/or namespace columns for participant-ids - also to support efficient lookup
|
||||||
|
// of "what parties a particular participant hosts" (ParticipantId => Set[PartyId])
|
||||||
|
case ptp: PartyToParticipantX
|
||||||
|
if filterParticipant.isEmpty || ptp.participants
|
||||||
|
.exists(
|
||||||
|
_.participantId.uid
|
||||||
|
.matchesPrefixes(prefixParticipantIdentifier, prefixParticipantNS)
|
||||||
|
) =>
|
||||||
|
Set(ptp.partyId)
|
||||||
|
case cert: DomainTrustCertificateX
|
||||||
|
if filterParty.isEmpty || cert.participantId.adminParty.uid
|
||||||
|
.matchesPrefixes(prefixPartyIdentifier, prefixPartyNS) =>
|
||||||
|
Set(cert.participantId.adminParty)
|
||||||
|
case _ => Set.empty
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findPositiveTransactions(
|
||||||
|
asOf: CantonTimestamp,
|
||||||
|
asOfInclusive: Boolean,
|
||||||
|
isProposal: Boolean,
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
filterUid: Option[Seq[UniqueIdentifier]],
|
||||||
|
filterNamespace: Option[Seq[Namespace]],
|
||||||
|
)(implicit traceContext: TraceContext): Future[PositiveStoredTopologyTransactionsX] =
|
||||||
|
findTransactionsBatchingUidFilter(
|
||||||
|
asOf,
|
||||||
|
asOfInclusive,
|
||||||
|
isProposal,
|
||||||
|
types.toSet,
|
||||||
|
filterUid,
|
||||||
|
filterNamespace,
|
||||||
|
TopologyChangeOpX.Replace.some,
|
||||||
|
).map(_.collectOfType[TopologyChangeOpX.Replace])
|
||||||
|
|
||||||
|
override def findFirstSequencerStateForSequencer(sequencerId: SequencerId)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[Replace, SequencerDomainStateX]]] = {
|
||||||
|
logger.debug(s"Querying first sequencer state for $sequencerId")
|
||||||
|
|
||||||
|
queryForTransactions(
|
||||||
|
// We don't expect too many MediatorDomainStateX mappings in a single domain, so fetching them all from the db
|
||||||
|
// is acceptable and also because we don't expect to run this query frequently. We can only evaluate the
|
||||||
|
// `mediatorId` field locally as the mediator-id is not exposed in a separate column.
|
||||||
|
sql" AND is_proposal = false" ++
|
||||||
|
sql" AND operation = ${TopologyChangeOpX.Replace}" ++
|
||||||
|
sql" AND transaction_type = ${SequencerDomainStateX.code}",
|
||||||
|
operation = "firstSequencerState",
|
||||||
|
).map(
|
||||||
|
_.collectOfMapping[SequencerDomainStateX]
|
||||||
|
.collectOfType[Replace]
|
||||||
|
.result
|
||||||
|
.filter {
|
||||||
|
_.mapping.allSequencers.contains(sequencerId)
|
||||||
|
}
|
||||||
|
.sortBy(_.serial)
|
||||||
|
.headOption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findFirstMediatorStateForMediator(mediatorId: MediatorId)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[Replace, MediatorDomainStateX]]] = {
|
||||||
|
logger.debug(s"Querying first mediator state for $mediatorId")
|
||||||
|
|
||||||
|
queryForTransactions(
|
||||||
|
// We don't expect too many MediatorDomainStateX mappings in a single domain, so fetching them all from the db
|
||||||
|
// is acceptable and also because we don't expect to run this query frequently. We can only evaluate the
|
||||||
|
// `mediatorId` field locally as the mediator-id is not exposed in a separate column.
|
||||||
|
sql" AND is_proposal = false" ++
|
||||||
|
sql" AND operation = ${TopologyChangeOpX.Replace}" ++
|
||||||
|
sql" AND transaction_type = ${MediatorDomainStateX.code}",
|
||||||
|
operation = "firstMediatorState",
|
||||||
|
).map(
|
||||||
|
_.collectOfMapping[MediatorDomainStateX]
|
||||||
|
.collectOfType[Replace]
|
||||||
|
.result
|
||||||
|
.collect {
|
||||||
|
case tx
|
||||||
|
if tx.mapping.observers.contains(mediatorId) ||
|
||||||
|
tx.mapping.active.contains(mediatorId) =>
|
||||||
|
tx
|
||||||
|
}
|
||||||
|
.sortBy(_.serial)
|
||||||
|
.headOption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findFirstTrustCertificateForParticipant(participant: ParticipantId)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[Replace, DomainTrustCertificateX]]] = {
|
||||||
|
logger.debug(s"Querying first trust certificate for participant $participant")
|
||||||
|
|
||||||
|
queryForTransactions(
|
||||||
|
sql" AND is_proposal = false" ++
|
||||||
|
sql" AND operation = ${TopologyChangeOpX.Replace}" ++
|
||||||
|
sql" AND transaction_type = ${DomainTrustCertificateX.code}" ++
|
||||||
|
sql" AND identifier = ${participant.uid.id} AND namespace = ${participant.uid.namespace}",
|
||||||
|
limit = storage.limit(1),
|
||||||
|
orderBy = " ORDER BY serial_counter ",
|
||||||
|
operation = "participantFirstTrustCertificate",
|
||||||
|
).map(
|
||||||
|
_.collectOfMapping[DomainTrustCertificateX]
|
||||||
|
.collectOfType[Replace]
|
||||||
|
.result
|
||||||
|
.headOption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findEssentialStateAtSequencedTime(
|
||||||
|
asOfInclusive: SequencedTime
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val timeFilter = sql" AND sequenced <= ${asOfInclusive.value}"
|
||||||
|
logger.debug(s"Querying essential state as of asOfInclusive")
|
||||||
|
|
||||||
|
queryForTransactions(timeFilter, "essentialState").map(
|
||||||
|
_.asSnapshotAtMaxEffectiveTime.retainAuthorizedHistoryAndEffectiveProposals
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def bootstrap(snapshot: GenericStoredTopologyTransactionsX)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Unit] =
|
||||||
|
// inserts must not be processed in parallel to keep the insertion order (as indicated by the `id` column)
|
||||||
|
// in sync with the monotonicity of sequenced
|
||||||
|
performBatchedDbOperation(snapshot.result, "bootstrap", processInParallel = false) { txs =>
|
||||||
|
insertSignedTransaction[GenericStoredTopologyTransactionX](TransactionEntry.fromStoredTx)(txs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findUpcomingEffectiveChanges(asOfInclusive: CantonTimestamp)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[TopologyStoreX.Change]] = {
|
||||||
|
logger.debug(s"Querying upcoming effective changes as of $asOfInclusive")
|
||||||
|
|
||||||
|
queryForTransactions(
|
||||||
|
sql" AND valid_from >= $asOfInclusive ",
|
||||||
|
orderBy = " ORDER BY valid_from",
|
||||||
|
operation = "upcomingEffectiveChanges",
|
||||||
|
).map(res => TopologyStoreX.accumulateUpcomingEffectiveChanges(res.result))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def maxTimestamp()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[(SequencedTime, EffectiveTime)]] = {
|
||||||
|
logger.debug(s"Querying max timestamp")
|
||||||
|
|
||||||
|
queryForTransactions(sql"", storage.limit(1), orderBy = " ORDER BY id DESC")
|
||||||
|
.map(_.result.headOption.map(tx => (tx.sequenced, tx.validFrom)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findDispatchingTransactionsAfter(
|
||||||
|
timestampExclusive: CantonTimestamp,
|
||||||
|
limitO: Option[Int],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val subQuery =
|
||||||
|
sql" AND valid_from > $timestampExclusive AND (not is_proposal OR valid_until is NULL)"
|
||||||
|
val limitQ = limitO.fold("")(storage.limit(_))
|
||||||
|
|
||||||
|
logger.debug(s"Querying dispatching transactions after $timestampExclusive")
|
||||||
|
|
||||||
|
queryForTransactions(subQuery, limitQ)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findStored(
|
||||||
|
asOfExclusive: CantonTimestamp,
|
||||||
|
transaction: GenericSignedTopologyTransactionX,
|
||||||
|
includeRejected: Boolean = false,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[GenericStoredTopologyTransactionX]] = {
|
||||||
|
logger.debug(s"Querying for transaction at $asOfExclusive: $transaction")
|
||||||
|
|
||||||
|
findStoredSql(asOfExclusive, transaction.transaction, includeRejected = includeRejected).map(
|
||||||
|
_.result.lastOption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findStoredForVersion(
|
||||||
|
asOfExclusive: CantonTimestamp,
|
||||||
|
transaction: GenericTopologyTransactionX,
|
||||||
|
protocolVersion: ProtocolVersion,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[GenericStoredTopologyTransactionX]] = {
|
||||||
|
val rpv = TopologyTransactionX.protocolVersionRepresentativeFor(protocolVersion)
|
||||||
|
|
||||||
|
logger.debug(s"Querying for transaction $transaction with protocol version $protocolVersion")
|
||||||
|
|
||||||
|
findStoredSql(
|
||||||
|
asOfExclusive,
|
||||||
|
transaction,
|
||||||
|
subQuery = sql" AND representative_protocol_version = ${rpv.representative}",
|
||||||
|
).map(_.result.lastOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findParticipantOnboardingTransactions(
|
||||||
|
participantId: ParticipantId,
|
||||||
|
domainId: DomainId,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): FutureUnlessShutdown[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
logger.debug(
|
||||||
|
s"Querying participant onboarding transactions for participant $participantId on domain $domainId"
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
data <- query
|
transactions <- FutureUnlessShutdown
|
||||||
.as[(PartyId, Option[String], Option[String], String255, CantonTimestamp, Boolean)]
|
.outcomeF(
|
||||||
} yield {
|
queryForTransactions(
|
||||||
data.map {
|
sql" AND not is_proposal " ++
|
||||||
case (partyId, displayNameS, participantIdS, submissionId, effectiveAt, notified) =>
|
sql" AND transaction_type IN (" ++ TopologyStoreX.initialParticipantDispatchingSet.toList
|
||||||
val participantId =
|
.map(s => sql"$s")
|
||||||
participantIdS
|
.intercalate(sql", ") ++ sql") ",
|
||||||
.flatMap(x => UniqueIdentifier.fromProtoPrimitive_(x).toOption)
|
operation = "participantOnboardingTransactions",
|
||||||
.map(ParticipantId(_))
|
|
||||||
val displayName = displayNameS.flatMap(String255.create(_).toOption)
|
|
||||||
PartyMetadata(
|
|
||||||
partyId,
|
|
||||||
displayName,
|
|
||||||
participantId = participantId,
|
|
||||||
)(
|
|
||||||
effectiveTimestamp = effectiveAt,
|
|
||||||
submissionId = submissionId,
|
|
||||||
notified = notified,
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
filteredTransactions = TopologyStoreX.filterInitialParticipantDispatchingTransactions(
|
||||||
|
participantId,
|
||||||
|
domainId,
|
||||||
|
transactions.result,
|
||||||
|
)
|
||||||
|
} yield filteredTransactions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert helper shared by bootstrap and update.
|
||||||
|
private def insertSignedTransaction[T](toTxEntry: T => TransactionEntry)(
|
||||||
|
transactions: Seq[T]
|
||||||
|
): SqlStreamingAction[Vector[Int], Int, slick.dbio.Effect.Write]#ResultAction[
|
||||||
|
Int,
|
||||||
|
NoStream,
|
||||||
|
Effect.Write,
|
||||||
|
] = {
|
||||||
|
def sqlTransactionParameters(transaction: T) = {
|
||||||
|
val txEntry = toTxEntry(transaction)
|
||||||
|
val signedTx = txEntry.signedTx
|
||||||
|
val validFrom = txEntry.validFrom.value
|
||||||
|
val validUntil = txEntry.validUntil.map(_.value)
|
||||||
|
val sequencedTs = txEntry.sequenced.value
|
||||||
|
val operation = signedTx.operation
|
||||||
|
val mapping = signedTx.mapping
|
||||||
|
val transactionType = mapping.code
|
||||||
|
val namespace = mapping.namespace
|
||||||
|
val identifier = mapping.maybeUid.map(_.id.toLengthLimitedString).getOrElse(String185.empty)
|
||||||
|
val serial = signedTx.serial
|
||||||
|
val mappingHash = mapping.uniqueKey.hash.toLengthLimitedHexString
|
||||||
|
val reason = txEntry.rejectionReason.map(_.asString1GB)
|
||||||
|
val txHash = signedTx.hash.hash.toLengthLimitedHexString
|
||||||
|
val isProposal = signedTx.isProposal
|
||||||
|
val representativeProtocolVersion = signedTx.transaction.representativeProtocolVersion
|
||||||
|
val hashOfSignatures = signedTx.hashOfSignatures.toLengthLimitedHexString
|
||||||
|
|
||||||
|
storage.profile match {
|
||||||
|
case _: DbStorage.Profile.Postgres | _: DbStorage.Profile.H2 =>
|
||||||
|
sql"""($transactionStoreIdName, $sequencedTs, $validFrom, $validUntil, $transactionType, $namespace,
|
||||||
|
$identifier, $mappingHash, $serial, $operation, $signedTx, $txHash, $isProposal, $reason, $representativeProtocolVersion, $hashOfSignatures)"""
|
||||||
|
case _: DbStorage.Profile.Oracle =>
|
||||||
|
throw new IllegalStateException("Oracle not supported by daml 3.0/X yet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(#14061): Decide whether we want additional indices by mapping_key_hash and tx_hash (e.g. for update/removal and lookups)
|
||||||
|
// TODO(#14061): Come up with columns/indexing for efficient ParticipantId => Seq[PartyId] lookup
|
||||||
|
// TODO(#12390) should mapping_key_hash rather be tx_hash?
|
||||||
|
storage.profile match {
|
||||||
|
case _: DbStorage.Profile.Postgres | _: DbStorage.Profile.H2 =>
|
||||||
|
(sql"""INSERT INTO common_topology_transactions (store_id, sequenced, valid_from, valid_until, transaction_type, namespace,
|
||||||
|
identifier, mapping_key_hash, serial_counter, operation, instance, tx_hash, is_proposal, rejection_reason, representative_protocol_version, hash_of_signatures) VALUES""" ++
|
||||||
|
transactions
|
||||||
|
.map(sqlTransactionParameters)
|
||||||
|
.toList
|
||||||
|
.intercalate(sql", ")
|
||||||
|
++ sql" ON CONFLICT DO NOTHING" // idempotency-"conflict" based on common_topology_transactions unique constraint
|
||||||
|
).asUpdate
|
||||||
|
case _: DbStorage.Profile.Oracle =>
|
||||||
|
throw new IllegalStateException("Oracle not supported by daml 3.0/X yet")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def insertOrUpdatePartyMetadata(
|
// Helper to break up large uid-filters into batches to limit the size of sql "in-clauses".
|
||||||
partyId: PartyId,
|
// Fashioned to reuse lessons learned in 2.x-based DbTopologyStore
|
||||||
participantId: Option[ParticipantId],
|
private def findTransactionsBatchingUidFilter(
|
||||||
displayName: Option[DisplayName],
|
asOf: CantonTimestamp,
|
||||||
effectiveTimestamp: CantonTimestamp,
|
asOfInclusive: Boolean,
|
||||||
submissionId: String255,
|
isProposal: Boolean,
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = {
|
types: Set[TopologyMappingX.Code],
|
||||||
val participantS = dbValue(participantId)
|
filterUid: Option[Seq[UniqueIdentifier]],
|
||||||
|
filterNamespace: Option[Seq[Namespace]],
|
||||||
|
filterOp: Option[TopologyChangeOpX],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
def forwardBatch(filterUidsNew: Option[Seq[UniqueIdentifier]]) =
|
||||||
|
findTransactionsSingleBatch(
|
||||||
|
asOf,
|
||||||
|
asOfInclusive,
|
||||||
|
isProposal,
|
||||||
|
types,
|
||||||
|
filterUidsNew,
|
||||||
|
filterNamespace,
|
||||||
|
filterOp,
|
||||||
|
)
|
||||||
|
|
||||||
|
filterUid.map(
|
||||||
|
// Optimization: remove uid-filters made redundant by namespace filters
|
||||||
|
_.filterNot(uid => filterNamespace.exists(_.contains(uid.namespace)))
|
||||||
|
) match {
|
||||||
|
case None => forwardBatch(None)
|
||||||
|
case Some(uids) =>
|
||||||
|
MonadUtil
|
||||||
|
.batchedSequentialTraverse(
|
||||||
|
parallelism = storage.threadsAvailableForWriting,
|
||||||
|
chunkSize = maxItemsInSqlQuery,
|
||||||
|
)(uids) { batchedUidFilters => forwardBatch(Some(batchedUidFilters)).map(_.result) }
|
||||||
|
.map(StoredTopologyTransactionsX(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def findTransactionsSingleBatch(
|
||||||
|
asOf: CantonTimestamp,
|
||||||
|
asOfInclusive: Boolean,
|
||||||
|
isProposal: Boolean,
|
||||||
|
types: Set[TopologyMappingX.Code],
|
||||||
|
filterUid: Option[Seq[UniqueIdentifier]],
|
||||||
|
filterNamespace: Option[Seq[Namespace]],
|
||||||
|
filterOp: Option[TopologyChangeOpX],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val hasUidFilter = filterUid.nonEmpty || filterNamespace.nonEmpty
|
||||||
|
// exit early if the caller produced an empty uid/namespace filter batch:
|
||||||
|
if (hasUidFilter && filterUid.forall(_.isEmpty) && filterNamespace.forall(_.isEmpty)) {
|
||||||
|
Future.successful(StoredTopologyTransactionsX.empty)
|
||||||
|
} else {
|
||||||
|
logger.debug(s"Querying transactions as of $asOf for types $types")
|
||||||
|
|
||||||
|
val timeRangeFilter = asOfQuery(asOf, asOfInclusive)
|
||||||
|
val isProposalFilter = sql" AND is_proposal = $isProposal"
|
||||||
|
val changeOpFilter = filterOp.fold(sql"")(op => sql" AND operation = $op")
|
||||||
|
val mappingTypeFilter = typeFilter(types)
|
||||||
|
val uidNamespaceFilter =
|
||||||
|
if (hasUidFilter) {
|
||||||
|
val namespaceFilter = filterNamespace.toList.flatMap(_.map(ns => sql"namespace = $ns"))
|
||||||
|
val uidFilter =
|
||||||
|
filterUid.toList.flatten.map(uid =>
|
||||||
|
sql"(identifier = ${uid.id} AND namespace = ${uid.namespace})"
|
||||||
|
)
|
||||||
|
sql" AND (" ++ (namespaceFilter ++ uidFilter).intercalate(sql" OR ") ++ sql")"
|
||||||
|
} else SQLActionBuilderChain(sql"")
|
||||||
|
|
||||||
|
queryForTransactions(
|
||||||
|
timeRangeFilter ++ isProposalFilter ++ changeOpFilter ++ mappingTypeFilter ++ uidNamespaceFilter,
|
||||||
|
operation = "singleBatch",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def typeFilter(types: Set[TopologyMappingX.Code]): SQLActionBuilderChain = {
|
||||||
|
if (types.isEmpty) sql""
|
||||||
|
else
|
||||||
|
sql" AND transaction_type IN (" ++ types.toSeq
|
||||||
|
.map(t => sql"$t")
|
||||||
|
.intercalate(sql", ") ++ sql")"
|
||||||
|
}
|
||||||
|
|
||||||
|
private def findAsOfExclusive(
|
||||||
|
effective: EffectiveTime,
|
||||||
|
subQuery: SQLActionBuilder,
|
||||||
|
operation: String,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
queryForTransactions(asOfQuery(effective.value, asOfInclusive = false) ++ subQuery, operation)
|
||||||
|
.map(_.result.map(_.transaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def findStoredSql(
|
||||||
|
asOfExclusive: CantonTimestamp,
|
||||||
|
transaction: GenericTopologyTransactionX,
|
||||||
|
subQuery: SQLActionBuilder = sql"",
|
||||||
|
includeRejected: Boolean = false,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val mapping = transaction.mapping
|
||||||
|
queryForTransactions(
|
||||||
|
// Query for leading fields of `idx_common_topology_transactions` to enable use of this index
|
||||||
|
sql" AND transaction_type = ${mapping.code} AND namespace = ${mapping.namespace} AND identifier = ${mapping.maybeUid
|
||||||
|
.fold(String185.empty)(_.id.toLengthLimitedString)}"
|
||||||
|
++ sql" AND valid_from < $asOfExclusive"
|
||||||
|
++ sql" AND mapping_key_hash = ${mapping.uniqueKey.hash.toLengthLimitedHexString}"
|
||||||
|
++ sql" AND serial_counter = ${transaction.serial}"
|
||||||
|
++ sql" AND tx_hash = ${transaction.hash.hash.toLengthLimitedHexString}"
|
||||||
|
++ sql" AND operation = ${transaction.operation}"
|
||||||
|
++ subQuery,
|
||||||
|
includeRejected = includeRejected,
|
||||||
|
operation = "findStored",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def queryForTransactions(
|
||||||
|
subQuery: SQLActionBuilder,
|
||||||
|
operation: String,
|
||||||
|
limit: String = "",
|
||||||
|
orderBy: String = " ORDER BY id ",
|
||||||
|
includeRejected: Boolean = false,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val query =
|
||||||
|
sql"SELECT instance, sequenced, valid_from, valid_until FROM common_topology_transactions WHERE store_id = $transactionStoreIdName" ++
|
||||||
|
subQuery ++ (if (!includeRejected) sql" AND rejection_reason IS NULL"
|
||||||
|
else sql"") ++ sql" #${orderBy} #${limit}"
|
||||||
|
|
||||||
|
storage
|
||||||
|
.query(
|
||||||
|
query.as[
|
||||||
|
(
|
||||||
|
GenericSignedTopologyTransactionX,
|
||||||
|
CantonTimestamp,
|
||||||
|
CantonTimestamp,
|
||||||
|
Option[CantonTimestamp],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
s"$functionFullName-$operation",
|
||||||
|
)
|
||||||
|
.map(_.map { case (tx, sequencedTs, validFrom, validUntil) =>
|
||||||
|
StoredTopologyTransactionX(
|
||||||
|
SequencedTime(sequencedTs),
|
||||||
|
EffectiveTime(validFrom),
|
||||||
|
validUntil.map(EffectiveTime(_)),
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(StoredTopologyTransactionsX(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def currentDispatchingWatermark(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[CantonTimestamp]] = {
|
||||||
|
val query =
|
||||||
|
sql"SELECT watermark_ts FROM common_topology_dispatching WHERE store_id =$transactionStoreIdName"
|
||||||
|
.as[CantonTimestamp]
|
||||||
|
.headOption
|
||||||
|
storage.query(query, functionFullName)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override def updateDispatchingWatermark(timestamp: CantonTimestamp)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Unit] = {
|
||||||
val query = storage.profile match {
|
val query = storage.profile match {
|
||||||
case _: DbStorage.Profile.Postgres =>
|
case _: DbStorage.Profile.Postgres =>
|
||||||
sqlu"""insert into common_party_metadata (party_id, display_name, participant_id, submission_id, effective_at)
|
sqlu"""insert into common_topology_dispatching (store_id, watermark_ts)
|
||||||
VALUES ($partyId, $displayName, $participantS, $submissionId, $effectiveTimestamp)
|
VALUES ($transactionStoreIdName, $timestamp)
|
||||||
on conflict (party_id) do update
|
on conflict (store_id) do update
|
||||||
set
|
set
|
||||||
display_name = $displayName,
|
watermark_ts = $timestamp
|
||||||
participant_id = $participantS,
|
|
||||||
submission_id = $submissionId,
|
|
||||||
effective_at = $effectiveTimestamp,
|
|
||||||
notified = false
|
|
||||||
"""
|
"""
|
||||||
case _: DbStorage.Profile.H2 | _: DbStorage.Profile.Oracle =>
|
case _: DbStorage.Profile.H2 | _: DbStorage.Profile.Oracle =>
|
||||||
sqlu"""merge into common_party_metadata
|
sqlu"""merge into common_topology_dispatching
|
||||||
using dual
|
using dual
|
||||||
on (party_id = $partyId)
|
on (store_id = $transactionStoreIdName)
|
||||||
when matched then
|
when matched then
|
||||||
update set
|
update set
|
||||||
display_name = $displayName,
|
watermark_ts = $timestamp
|
||||||
participant_id = $participantS,
|
|
||||||
submission_id = $submissionId,
|
|
||||||
effective_at = $effectiveTimestamp,
|
|
||||||
notified = ${false}
|
|
||||||
when not matched then
|
when not matched then
|
||||||
insert (party_id, display_name, participant_id, submission_id, effective_at)
|
insert (store_id, watermark_ts)
|
||||||
values ($partyId, $displayName, $participantS, $submissionId, $effectiveTimestamp)
|
values ($transactionStoreIdName, $timestamp)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
storage.update_(query, functionFullName)
|
storage.update_(query, functionFullName)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def dbValue(participantId: Option[ParticipantId]): Option[String300] =
|
private def asOfQuery(asOf: CantonTimestamp, asOfInclusive: Boolean): SQLActionBuilder =
|
||||||
participantId.map(_.uid.toLengthLimitedString.asString300)
|
if (asOfInclusive)
|
||||||
|
sql" AND valid_from <= $asOf AND (valid_until is NULL OR $asOf < valid_until)"
|
||||||
|
else
|
||||||
|
sql" AND valid_from < $asOf AND (valid_until is NULL OR $asOf <= valid_until)"
|
||||||
|
|
||||||
/** mark the given metadata has having been successfully forwarded to the domain */
|
private def getHeadStateQuery(
|
||||||
override def markNotified(
|
recentTimestampO: Option[CantonTimestamp]
|
||||||
metadata: PartyMetadata
|
): SQLActionBuilderChain = recentTimestampO match {
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = {
|
case Some(value) => asOfQuery(value, asOfInclusive = false)
|
||||||
val partyId = metadata.partyId
|
case None => sql" AND valid_until is NULL"
|
||||||
val effectiveAt = metadata.effectiveTimestamp
|
|
||||||
val query =
|
|
||||||
sqlu"UPDATE common_party_metadata SET notified = ${true} WHERE party_id = $partyId and effective_at = $effectiveAt"
|
|
||||||
storage.update_(query, functionFullName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** fetch the current set of party data which still needs to be notified */
|
@SuppressWarnings(Array("com.digitalasset.canton.SlickString"))
|
||||||
override def fetchNotNotified()(implicit
|
private def getIdFilter(
|
||||||
traceContext: TraceContext
|
idFilter: Option[String]
|
||||||
): Future[Seq[PartyMetadata]] = {
|
): SQLActionBuilderChain =
|
||||||
storage
|
idFilter match {
|
||||||
.query(
|
case Some(value) if value.nonEmpty => sql" AND identifier like ${value + "%"}"
|
||||||
metadataForPartyQuery(sql"notified = ${false}"),
|
case _ => sql""
|
||||||
functionFullName,
|
}
|
||||||
)
|
|
||||||
}
|
@SuppressWarnings(Array("com.digitalasset.canton.SlickString"))
|
||||||
|
private def getNamespaceFilter(namespaceFilter: Option[String]): SQLActionBuilderChain =
|
||||||
|
namespaceFilter match {
|
||||||
|
case Some(value) if value.nonEmpty => sql" AND namespace LIKE ${value + "%"}"
|
||||||
|
case _ => sql""
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper case class to hold StoredTopologyTransactionX-fields in update() providing umbrella
|
||||||
|
// values for all transactions.
|
||||||
|
private[db] final case class TransactionEntry(
|
||||||
|
sequenced: SequencedTime,
|
||||||
|
validFrom: EffectiveTime,
|
||||||
|
validUntil: Option[EffectiveTime],
|
||||||
|
signedTx: GenericSignedTopologyTransactionX,
|
||||||
|
rejectionReason: Option[TopologyTransactionRejection] = None,
|
||||||
|
)
|
||||||
|
|
||||||
|
private[db] object TransactionEntry {
|
||||||
|
def fromStoredTx(stx: GenericStoredTopologyTransactionX): TransactionEntry = TransactionEntry(
|
||||||
|
stx.sequenced,
|
||||||
|
stx.validFrom,
|
||||||
|
stx.validUntil,
|
||||||
|
stx.transaction,
|
||||||
|
rejectionReason = None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,873 +0,0 @@
|
|||||||
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package com.digitalasset.canton.topology.store.db
|
|
||||||
|
|
||||||
import cats.syntax.option.*
|
|
||||||
import com.daml.nameof.NameOf.functionFullName
|
|
||||||
import com.daml.nonempty.NonEmpty
|
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.{LengthLimitedString, String185}
|
|
||||||
import com.digitalasset.canton.config.ProcessingTimeout
|
|
||||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
|
||||||
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
|
|
||||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
|
||||||
import com.digitalasset.canton.resource.DbStorage.SQLActionBuilderChain
|
|
||||||
import com.digitalasset.canton.resource.{DbStorage, DbStore}
|
|
||||||
import com.digitalasset.canton.topology.*
|
|
||||||
import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime}
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionX.GenericStoredTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.{
|
|
||||||
GenericStoredTopologyTransactionsX,
|
|
||||||
PositiveStoredTopologyTransactionsX,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.store.ValidatedTopologyTransactionX.GenericValidatedTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.store.*
|
|
||||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.{
|
|
||||||
GenericSignedTopologyTransactionX,
|
|
||||||
setParameterTopologyTransaction,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyChangeOpX.Replace
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyTransactionX.{
|
|
||||||
GenericTopologyTransactionX,
|
|
||||||
TxHash,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.transaction.*
|
|
||||||
import com.digitalasset.canton.tracing.TraceContext
|
|
||||||
import com.digitalasset.canton.util.FutureInstances.*
|
|
||||||
import com.digitalasset.canton.util.MonadUtil
|
|
||||||
import com.digitalasset.canton.version.ProtocolVersion
|
|
||||||
import com.google.common.annotations.VisibleForTesting
|
|
||||||
import slick.jdbc.canton.SQLActionBuilder
|
|
||||||
import slick.jdbc.{GetResult, TransactionIsolation}
|
|
||||||
import slick.sql.SqlStreamingAction
|
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
|
||||||
|
|
||||||
class DbTopologyStoreX[StoreId <: TopologyStoreId](
|
|
||||||
override protected val storage: DbStorage,
|
|
||||||
val storeId: StoreId,
|
|
||||||
override protected val timeouts: ProcessingTimeout,
|
|
||||||
override protected val loggerFactory: NamedLoggerFactory,
|
|
||||||
protected val maxItemsInSqlQuery: PositiveInt = PositiveInt.tryCreate(100),
|
|
||||||
)(implicit ec: ExecutionContext)
|
|
||||||
extends TopologyStoreX[StoreId]
|
|
||||||
with DbStore {
|
|
||||||
|
|
||||||
import DbStorage.Implicits.BuilderChain.*
|
|
||||||
import storage.api.*
|
|
||||||
import storage.converters.*
|
|
||||||
|
|
||||||
private implicit val getResultSignedTopologyTransaction
|
|
||||||
: GetResult[GenericSignedTopologyTransactionX] =
|
|
||||||
SignedTopologyTransactionX.createGetResultDomainTopologyTransaction
|
|
||||||
|
|
||||||
protected val transactionStoreIdName: LengthLimitedString = storeId.dbString
|
|
||||||
|
|
||||||
def findTransactionsByTxHash(asOfExclusive: EffectiveTime, hashes: Set[TxHash])(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
|
|
||||||
if (hashes.isEmpty) Future.successful(Seq.empty)
|
|
||||||
else {
|
|
||||||
logger.debug(s"Querying transactions for tx hashes $hashes as of $asOfExclusive")
|
|
||||||
|
|
||||||
findAsOfExclusive(
|
|
||||||
asOfExclusive,
|
|
||||||
sql" AND (" ++ hashes
|
|
||||||
.map(txHash => sql"tx_hash = ${txHash.hash.toLengthLimitedHexString}")
|
|
||||||
.toList
|
|
||||||
.intercalate(sql" OR ") ++ sql")",
|
|
||||||
operation = "transactionsByTxHash",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findProposalsByTxHash(
|
|
||||||
asOfExclusive: EffectiveTime,
|
|
||||||
hashes: NonEmpty[Set[TxHash]],
|
|
||||||
)(implicit traceContext: TraceContext): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
logger.debug(s"Querying proposals for tx hashes $hashes as of $asOfExclusive")
|
|
||||||
|
|
||||||
findAsOfExclusive(
|
|
||||||
asOfExclusive,
|
|
||||||
sql" AND is_proposal = true AND (" ++ hashes
|
|
||||||
.map(txHash => sql"tx_hash = ${txHash.hash.toLengthLimitedHexString}")
|
|
||||||
.forgetNE
|
|
||||||
.toList
|
|
||||||
.intercalate(sql" OR ") ++ sql")",
|
|
||||||
"proposalsByTxHash",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findTransactionsForMapping(
|
|
||||||
asOfExclusive: EffectiveTime,
|
|
||||||
hashes: NonEmpty[Set[MappingHash]],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
logger.debug(s"Querying proposals for mapping hashes $hashes as of $asOfExclusive")
|
|
||||||
|
|
||||||
findAsOfExclusive(
|
|
||||||
asOfExclusive,
|
|
||||||
sql" AND is_proposal = false AND (" ++ hashes
|
|
||||||
.map(mappingHash => sql"mapping_key_hash = ${mappingHash.hash.toLengthLimitedHexString}")
|
|
||||||
.forgetNE
|
|
||||||
.toList
|
|
||||||
.intercalate(sql" OR ") ++ sql")",
|
|
||||||
operation = "transactionsForMapping",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param elements Elements to be batched
|
|
||||||
* @param operationName Name of the operation
|
|
||||||
* @param f Create a DBIOAction from a batch
|
|
||||||
*/
|
|
||||||
private def performBatchedDbOperation[X](
|
|
||||||
elements: Seq[X],
|
|
||||||
operationName: String,
|
|
||||||
processInParallel: Boolean,
|
|
||||||
)(
|
|
||||||
f: Seq[X] => DBIOAction[_, NoStream, Effect.Write with Effect.Transactional]
|
|
||||||
)(implicit traceContext: TraceContext) = if (elements.isEmpty) Future.successful(())
|
|
||||||
else
|
|
||||||
MonadUtil.batchedSequentialTraverse_(
|
|
||||||
parallelism =
|
|
||||||
if (processInParallel) PositiveInt.two * storage.threadsAvailableForWriting
|
|
||||||
else PositiveInt.one,
|
|
||||||
chunkSize = maxItemsInSqlQuery,
|
|
||||||
)(elements) { elementsBatch =>
|
|
||||||
storage.update_(
|
|
||||||
f(elementsBatch),
|
|
||||||
operationName = operationName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** add validated topology transaction as is to the topology transaction table
|
|
||||||
*/
|
|
||||||
override def update(
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
effective: EffectiveTime,
|
|
||||||
removeMapping: Map[TopologyMappingX.MappingHash, PositiveInt],
|
|
||||||
removeTxs: Set[TopologyTransactionX.TxHash],
|
|
||||||
additions: Seq[GenericValidatedTopologyTransactionX],
|
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = {
|
|
||||||
|
|
||||||
val effectiveTs = effective.value
|
|
||||||
|
|
||||||
val transactionRemovals = removeMapping.toList.map { case (mappingHash, serial) =>
|
|
||||||
sql"mapping_key_hash=${mappingHash.hash.toLengthLimitedHexString} and serial_counter <= $serial"
|
|
||||||
} ++ removeTxs.map(txHash => sql"tx_hash=${txHash.hash.toLengthLimitedHexString}")
|
|
||||||
|
|
||||||
lazy val updateRemovals =
|
|
||||||
(sql"UPDATE common_topology_transactions SET valid_until = ${Some(effectiveTs)} WHERE store_id=$transactionStoreIdName AND (" ++
|
|
||||||
transactionRemovals
|
|
||||||
.intercalate(
|
|
||||||
sql" OR "
|
|
||||||
) ++ sql") AND valid_from < $effectiveTs AND valid_until is null").asUpdate
|
|
||||||
|
|
||||||
lazy val insertAdditions =
|
|
||||||
insertSignedTransaction[GenericValidatedTopologyTransactionX](vtx =>
|
|
||||||
TransactionEntry(
|
|
||||||
sequenced,
|
|
||||||
effective,
|
|
||||||
Option.when(
|
|
||||||
vtx.rejectionReason.nonEmpty || vtx.expireImmediately
|
|
||||||
)(effective),
|
|
||||||
vtx.transaction,
|
|
||||||
vtx.rejectionReason,
|
|
||||||
)
|
|
||||||
)(additions)
|
|
||||||
|
|
||||||
storage.update_(
|
|
||||||
DBIO
|
|
||||||
.seq(
|
|
||||||
if (transactionRemovals.nonEmpty) updateRemovals else DBIO.successful(0),
|
|
||||||
if (additions.nonEmpty) insertAdditions
|
|
||||||
else DBIO.successful(0),
|
|
||||||
)
|
|
||||||
.transactionally
|
|
||||||
.withTransactionIsolation(TransactionIsolation.Serializable),
|
|
||||||
operationName = "update-topology-transactions",
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
override protected[topology] def dumpStoreContent()(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
// Helper case class to produce comparable output to the InMemoryStore
|
|
||||||
case class TopologyStoreEntry(
|
|
||||||
transaction: GenericSignedTopologyTransactionX,
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
from: EffectiveTime,
|
|
||||||
until: Option[EffectiveTime],
|
|
||||||
rejected: Option[String],
|
|
||||||
)
|
|
||||||
|
|
||||||
val query =
|
|
||||||
sql"SELECT instance, sequenced, valid_from, valid_until, rejection_reason FROM common_topology_transactions WHERE store_id = $transactionStoreIdName ORDER BY id"
|
|
||||||
|
|
||||||
val entriesF =
|
|
||||||
storage
|
|
||||||
.query(
|
|
||||||
query.as[
|
|
||||||
(
|
|
||||||
GenericSignedTopologyTransactionX,
|
|
||||||
CantonTimestamp,
|
|
||||||
CantonTimestamp,
|
|
||||||
Option[CantonTimestamp],
|
|
||||||
Option[String],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
functionFullName,
|
|
||||||
)
|
|
||||||
.map(_.map { case (tx, sequencedTs, validFrom, validUntil, rejectionReason) =>
|
|
||||||
TopologyStoreEntry(
|
|
||||||
tx,
|
|
||||||
SequencedTime(sequencedTs),
|
|
||||||
EffectiveTime(validFrom),
|
|
||||||
validUntil.map(EffectiveTime(_)),
|
|
||||||
rejectionReason,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
entriesF.map { entries =>
|
|
||||||
logger.debug(
|
|
||||||
entries
|
|
||||||
.map(_.toString)
|
|
||||||
.mkString("Topology Store Content[", ", ", "]")
|
|
||||||
)
|
|
||||||
StoredTopologyTransactionsX(
|
|
||||||
entries.map(e => StoredTopologyTransactionX(e.sequenced, e.from, e.until, e.transaction))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def inspect(
|
|
||||||
proposals: Boolean,
|
|
||||||
timeQuery: TimeQuery,
|
|
||||||
recentTimestampO: Option[CantonTimestamp],
|
|
||||||
op: Option[TopologyChangeOpX],
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
idFilter: Option[String],
|
|
||||||
namespaceFilter: Option[String],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]] = {
|
|
||||||
logger.debug(s"Inspecting store for types=$types, filter=$idFilter, time=$timeQuery")
|
|
||||||
|
|
||||||
val timeFilter: SQLActionBuilderChain = timeQuery match {
|
|
||||||
case TimeQuery.HeadState =>
|
|
||||||
getHeadStateQuery(recentTimestampO)
|
|
||||||
case TimeQuery.Snapshot(asOf) =>
|
|
||||||
asOfQuery(asOf = asOf, asOfInclusive = false)
|
|
||||||
case TimeQuery.Range(None, None) =>
|
|
||||||
sql"" // The case below inserts an additional `AND` that we don't want
|
|
||||||
case TimeQuery.Range(from, until) =>
|
|
||||||
sql" AND " ++ ((from.toList.map(ts => sql"valid_from >= $ts") ++ until.toList.map(ts =>
|
|
||||||
sql"valid_from <= $ts"
|
|
||||||
))
|
|
||||||
.intercalate(sql" AND "))
|
|
||||||
}
|
|
||||||
|
|
||||||
val operationFilter = op.map(value => sql" AND operation = $value").getOrElse(sql"")
|
|
||||||
|
|
||||||
val mappingIdFilter = getIdFilter(idFilter)
|
|
||||||
val mappingNameSpaceFilter = getNamespaceFilter(namespaceFilter)
|
|
||||||
|
|
||||||
val mappingTypeFilter = typeFilter(types.toSet)
|
|
||||||
|
|
||||||
val mappingProposalsAndPreviousFilter =
|
|
||||||
timeFilter ++ operationFilter ++ mappingIdFilter ++ mappingNameSpaceFilter ++ mappingTypeFilter ++ sql" AND is_proposal = $proposals"
|
|
||||||
|
|
||||||
queryForTransactions(mappingProposalsAndPreviousFilter, "inspect")
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("com.digitalasset.canton.SlickString"))
|
|
||||||
override def inspectKnownParties(
|
|
||||||
timestamp: CantonTimestamp,
|
|
||||||
filterParty: String,
|
|
||||||
filterParticipant: String,
|
|
||||||
limit: Int,
|
|
||||||
)(implicit traceContext: TraceContext): Future[Set[PartyId]] = {
|
|
||||||
logger.debug(
|
|
||||||
s"Inspecting known parties at t=$timestamp with filterParty=$filterParty and filterParticipant=$filterParticipant"
|
|
||||||
)
|
|
||||||
|
|
||||||
def splitFilterPrefixAndSql(uidFilter: String): (String, String, String, String) =
|
|
||||||
UniqueIdentifier.splitFilter(uidFilter) match {
|
|
||||||
case (id, ns) => (id, ns, id + "%", ns + "%")
|
|
||||||
}
|
|
||||||
|
|
||||||
val (prefixPartyIdentifier, prefixPartyNS, sqlPartyIdentifier, sqlPartyNS) =
|
|
||||||
splitFilterPrefixAndSql(filterParty)
|
|
||||||
val (
|
|
||||||
prefixParticipantIdentifier,
|
|
||||||
prefixParticipantNS,
|
|
||||||
sqlParticipantIdentifier,
|
|
||||||
sqlParticipantNS,
|
|
||||||
) =
|
|
||||||
splitFilterPrefixAndSql(filterParticipant)
|
|
||||||
|
|
||||||
// conditional append avoids "like '%'" filters on empty filters
|
|
||||||
def conditionalAppend(filter: String, sqlIdentifier: String, sqlNamespace: String) =
|
|
||||||
if (filter.nonEmpty)
|
|
||||||
sql" AND identifier LIKE ${sqlIdentifier} AND namespace LIKE ${sqlNamespace}"
|
|
||||||
else sql""
|
|
||||||
|
|
||||||
queryForTransactions(
|
|
||||||
asOfQuery(timestamp, asOfInclusive = false) ++
|
|
||||||
sql" AND NOT is_proposal AND operation = ${TopologyChangeOpX.Replace} AND ("
|
|
||||||
// PartyToParticipantX filtering
|
|
||||||
++ Seq(
|
|
||||||
sql"(transaction_type = ${PartyToParticipantX.code}"
|
|
||||||
++ conditionalAppend(filterParty, sqlPartyIdentifier, sqlPartyNS)
|
|
||||||
++ sql")"
|
|
||||||
)
|
|
||||||
++ sql" OR "
|
|
||||||
// DomainTrustCertificateX filtering
|
|
||||||
++ Seq(
|
|
||||||
sql"(transaction_type = ${DomainTrustCertificateX.code}"
|
|
||||||
// In DomainTrustCertificateX part of the filter, compare not only to participant, but also to party identifier
|
|
||||||
// to enable searching for the admin party
|
|
||||||
++ conditionalAppend(filterParty, sqlPartyIdentifier, sqlPartyNS)
|
|
||||||
++ conditionalAppend(filterParticipant, sqlParticipantIdentifier, sqlParticipantNS)
|
|
||||||
++ sql")"
|
|
||||||
)
|
|
||||||
++ sql")",
|
|
||||||
storage.limit(limit),
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
_.result.toSet
|
|
||||||
.flatMap[PartyId](_.mapping match {
|
|
||||||
// TODO(#14061): post-filtering for participantId non-columns results in fewer than limit results being returned
|
|
||||||
// - add indexed secondary uid and/or namespace columns for participant-ids - also to support efficient lookup
|
|
||||||
// of "what parties a particular participant hosts" (ParticipantId => Set[PartyId])
|
|
||||||
case ptp: PartyToParticipantX
|
|
||||||
if filterParticipant.isEmpty || ptp.participants
|
|
||||||
.exists(
|
|
||||||
_.participantId.uid
|
|
||||||
.matchesPrefixes(prefixParticipantIdentifier, prefixParticipantNS)
|
|
||||||
) =>
|
|
||||||
Set(ptp.partyId)
|
|
||||||
case cert: DomainTrustCertificateX
|
|
||||||
if filterParty.isEmpty || cert.participantId.adminParty.uid
|
|
||||||
.matchesPrefixes(prefixPartyIdentifier, prefixPartyNS) =>
|
|
||||||
Set(cert.participantId.adminParty)
|
|
||||||
case _ => Set.empty
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findPositiveTransactions(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
isProposal: Boolean,
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
filterUid: Option[Seq[UniqueIdentifier]],
|
|
||||||
filterNamespace: Option[Seq[Namespace]],
|
|
||||||
)(implicit traceContext: TraceContext): Future[PositiveStoredTopologyTransactionsX] =
|
|
||||||
findTransactionsBatchingUidFilter(
|
|
||||||
asOf,
|
|
||||||
asOfInclusive,
|
|
||||||
isProposal,
|
|
||||||
types.toSet,
|
|
||||||
filterUid,
|
|
||||||
filterNamespace,
|
|
||||||
TopologyChangeOpX.Replace.some,
|
|
||||||
).map(_.collectOfType[TopologyChangeOpX.Replace])
|
|
||||||
|
|
||||||
override def findFirstSequencerStateForSequencer(sequencerId: SequencerId)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[Replace, SequencerDomainStateX]]] = {
|
|
||||||
logger.debug(s"Querying first sequencer state for $sequencerId")
|
|
||||||
|
|
||||||
queryForTransactions(
|
|
||||||
// We don't expect too many MediatorDomainStateX mappings in a single domain, so fetching them all from the db
|
|
||||||
// is acceptable and also because we don't expect to run this query frequently. We can only evaluate the
|
|
||||||
// `mediatorId` field locally as the mediator-id is not exposed in a separate column.
|
|
||||||
sql" AND is_proposal = false" ++
|
|
||||||
sql" AND operation = ${TopologyChangeOpX.Replace}" ++
|
|
||||||
sql" AND transaction_type = ${SequencerDomainStateX.code}",
|
|
||||||
operation = "firstSequencerState",
|
|
||||||
).map(
|
|
||||||
_.collectOfMapping[SequencerDomainStateX]
|
|
||||||
.collectOfType[Replace]
|
|
||||||
.result
|
|
||||||
.filter {
|
|
||||||
_.mapping.allSequencers.contains(sequencerId)
|
|
||||||
}
|
|
||||||
.sortBy(_.serial)
|
|
||||||
.headOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findFirstMediatorStateForMediator(mediatorId: MediatorId)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[Replace, MediatorDomainStateX]]] = {
|
|
||||||
logger.debug(s"Querying first mediator state for $mediatorId")
|
|
||||||
|
|
||||||
queryForTransactions(
|
|
||||||
// We don't expect too many MediatorDomainStateX mappings in a single domain, so fetching them all from the db
|
|
||||||
// is acceptable and also because we don't expect to run this query frequently. We can only evaluate the
|
|
||||||
// `mediatorId` field locally as the mediator-id is not exposed in a separate column.
|
|
||||||
sql" AND is_proposal = false" ++
|
|
||||||
sql" AND operation = ${TopologyChangeOpX.Replace}" ++
|
|
||||||
sql" AND transaction_type = ${MediatorDomainStateX.code}",
|
|
||||||
operation = "firstMediatorState",
|
|
||||||
).map(
|
|
||||||
_.collectOfMapping[MediatorDomainStateX]
|
|
||||||
.collectOfType[Replace]
|
|
||||||
.result
|
|
||||||
.collect {
|
|
||||||
case tx
|
|
||||||
if tx.mapping.observers.contains(mediatorId) ||
|
|
||||||
tx.mapping.active.contains(mediatorId) =>
|
|
||||||
tx
|
|
||||||
}
|
|
||||||
.sortBy(_.serial)
|
|
||||||
.headOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findFirstTrustCertificateForParticipant(participant: ParticipantId)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[Replace, DomainTrustCertificateX]]] = {
|
|
||||||
logger.debug(s"Querying first trust certificate for participant $participant")
|
|
||||||
|
|
||||||
queryForTransactions(
|
|
||||||
sql" AND is_proposal = false" ++
|
|
||||||
sql" AND operation = ${TopologyChangeOpX.Replace}" ++
|
|
||||||
sql" AND transaction_type = ${DomainTrustCertificateX.code}" ++
|
|
||||||
sql" AND identifier = ${participant.uid.id} AND namespace = ${participant.uid.namespace}",
|
|
||||||
limit = storage.limit(1),
|
|
||||||
orderBy = " ORDER BY serial_counter ",
|
|
||||||
operation = "participantFirstTrustCertificate",
|
|
||||||
).map(
|
|
||||||
_.collectOfMapping[DomainTrustCertificateX]
|
|
||||||
.collectOfType[Replace]
|
|
||||||
.result
|
|
||||||
.headOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findEssentialStateAtSequencedTime(
|
|
||||||
asOfInclusive: SequencedTime
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val timeFilter = sql" AND sequenced <= ${asOfInclusive.value}"
|
|
||||||
logger.debug(s"Querying essential state as of asOfInclusive")
|
|
||||||
|
|
||||||
queryForTransactions(timeFilter, "essentialState").map(
|
|
||||||
_.asSnapshotAtMaxEffectiveTime.retainAuthorizedHistoryAndEffectiveProposals
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def bootstrap(snapshot: GenericStoredTopologyTransactionsX)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Unit] =
|
|
||||||
// inserts must not be processed in parallel to keep the insertion order (as indicated by the `id` column)
|
|
||||||
// in sync with the monotonicity of sequenced
|
|
||||||
performBatchedDbOperation(snapshot.result, "bootstrap", processInParallel = false) { txs =>
|
|
||||||
insertSignedTransaction[GenericStoredTopologyTransactionX](TransactionEntry.fromStoredTx)(txs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findUpcomingEffectiveChanges(asOfInclusive: CantonTimestamp)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[TopologyStoreX.Change]] = {
|
|
||||||
logger.debug(s"Querying upcoming effective changes as of $asOfInclusive")
|
|
||||||
|
|
||||||
queryForTransactions(
|
|
||||||
sql" AND valid_from >= $asOfInclusive ",
|
|
||||||
orderBy = " ORDER BY valid_from",
|
|
||||||
operation = "upcomingEffectiveChanges",
|
|
||||||
).map(res => TopologyStoreX.accumulateUpcomingEffectiveChanges(res.result))
|
|
||||||
}
|
|
||||||
|
|
||||||
override def maxTimestamp()(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[(SequencedTime, EffectiveTime)]] = {
|
|
||||||
logger.debug(s"Querying max timestamp")
|
|
||||||
|
|
||||||
queryForTransactions(sql"", storage.limit(1), orderBy = " ORDER BY id DESC")
|
|
||||||
.map(_.result.headOption.map(tx => (tx.sequenced, tx.validFrom)))
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findDispatchingTransactionsAfter(
|
|
||||||
timestampExclusive: CantonTimestamp,
|
|
||||||
limitO: Option[Int],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val subQuery =
|
|
||||||
sql" AND valid_from > $timestampExclusive AND (not is_proposal OR valid_until is NULL)"
|
|
||||||
val limitQ = limitO.fold("")(storage.limit(_))
|
|
||||||
|
|
||||||
logger.debug(s"Querying dispatching transactions after $timestampExclusive")
|
|
||||||
|
|
||||||
queryForTransactions(subQuery, limitQ)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findStored(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericSignedTopologyTransactionX,
|
|
||||||
includeRejected: Boolean = false,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[GenericStoredTopologyTransactionX]] = {
|
|
||||||
logger.debug(s"Querying for transaction at $asOfExclusive: $transaction")
|
|
||||||
|
|
||||||
findStoredSql(asOfExclusive, transaction.transaction, includeRejected = includeRejected).map(
|
|
||||||
_.result.lastOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findStoredForVersion(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericTopologyTransactionX,
|
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[GenericStoredTopologyTransactionX]] = {
|
|
||||||
val rpv = TopologyTransactionX.protocolVersionRepresentativeFor(protocolVersion)
|
|
||||||
|
|
||||||
logger.debug(s"Querying for transaction $transaction with protocol version $protocolVersion")
|
|
||||||
|
|
||||||
findStoredSql(
|
|
||||||
asOfExclusive,
|
|
||||||
transaction,
|
|
||||||
subQuery = sql" AND representative_protocol_version = ${rpv.representative}",
|
|
||||||
).map(_.result.lastOption)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findParticipantOnboardingTransactions(
|
|
||||||
participantId: ParticipantId,
|
|
||||||
domainId: DomainId,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): FutureUnlessShutdown[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
logger.debug(
|
|
||||||
s"Querying participant onboarding transactions for participant $participantId on domain $domainId"
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
transactions <- FutureUnlessShutdown
|
|
||||||
.outcomeF(
|
|
||||||
queryForTransactions(
|
|
||||||
sql" AND not is_proposal " ++
|
|
||||||
sql" AND transaction_type IN (" ++ TopologyStoreX.initialParticipantDispatchingSet.toList
|
|
||||||
.map(s => sql"$s")
|
|
||||||
.intercalate(sql", ") ++ sql") ",
|
|
||||||
operation = "participantOnboardingTransactions",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
filteredTransactions = TopologyStoreX.filterInitialParticipantDispatchingTransactions(
|
|
||||||
participantId,
|
|
||||||
domainId,
|
|
||||||
transactions.result,
|
|
||||||
)
|
|
||||||
} yield filteredTransactions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert helper shared by bootstrap and update.
|
|
||||||
private def insertSignedTransaction[T](toTxEntry: T => TransactionEntry)(
|
|
||||||
transactions: Seq[T]
|
|
||||||
): SqlStreamingAction[Vector[Int], Int, slick.dbio.Effect.Write]#ResultAction[
|
|
||||||
Int,
|
|
||||||
NoStream,
|
|
||||||
Effect.Write,
|
|
||||||
] = {
|
|
||||||
def sqlTransactionParameters(transaction: T) = {
|
|
||||||
val txEntry = toTxEntry(transaction)
|
|
||||||
val signedTx = txEntry.signedTx
|
|
||||||
val validFrom = txEntry.validFrom.value
|
|
||||||
val validUntil = txEntry.validUntil.map(_.value)
|
|
||||||
val sequencedTs = txEntry.sequenced.value
|
|
||||||
val operation = signedTx.operation
|
|
||||||
val mapping = signedTx.mapping
|
|
||||||
val transactionType = mapping.code
|
|
||||||
val namespace = mapping.namespace
|
|
||||||
val identifier = mapping.maybeUid.map(_.id.toLengthLimitedString).getOrElse(String185.empty)
|
|
||||||
val serial = signedTx.serial
|
|
||||||
val mappingHash = mapping.uniqueKey.hash.toLengthLimitedHexString
|
|
||||||
val reason = txEntry.rejectionReason.map(_.asString1GB)
|
|
||||||
val txHash = signedTx.hash.hash.toLengthLimitedHexString
|
|
||||||
val isProposal = signedTx.isProposal
|
|
||||||
val representativeProtocolVersion = signedTx.transaction.representativeProtocolVersion
|
|
||||||
val hashOfSignatures = signedTx.hashOfSignatures.toLengthLimitedHexString
|
|
||||||
|
|
||||||
storage.profile match {
|
|
||||||
case _: DbStorage.Profile.Postgres | _: DbStorage.Profile.H2 =>
|
|
||||||
sql"""($transactionStoreIdName, $sequencedTs, $validFrom, $validUntil, $transactionType, $namespace,
|
|
||||||
$identifier, $mappingHash, $serial, $operation, $signedTx, $txHash, $isProposal, $reason, $representativeProtocolVersion, $hashOfSignatures)"""
|
|
||||||
case _: DbStorage.Profile.Oracle =>
|
|
||||||
throw new IllegalStateException("Oracle not supported by daml 3.0/X yet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(#14061): Decide whether we want additional indices by mapping_key_hash and tx_hash (e.g. for update/removal and lookups)
|
|
||||||
// TODO(#14061): Come up with columns/indexing for efficient ParticipantId => Seq[PartyId] lookup
|
|
||||||
// TODO(#12390) should mapping_key_hash rather be tx_hash?
|
|
||||||
storage.profile match {
|
|
||||||
case _: DbStorage.Profile.Postgres | _: DbStorage.Profile.H2 =>
|
|
||||||
(sql"""INSERT INTO common_topology_transactions (store_id, sequenced, valid_from, valid_until, transaction_type, namespace,
|
|
||||||
identifier, mapping_key_hash, serial_counter, operation, instance, tx_hash, is_proposal, rejection_reason, representative_protocol_version, hash_of_signatures) VALUES""" ++
|
|
||||||
transactions
|
|
||||||
.map(sqlTransactionParameters)
|
|
||||||
.toList
|
|
||||||
.intercalate(sql", ")
|
|
||||||
++ sql" ON CONFLICT DO NOTHING" // idempotency-"conflict" based on common_topology_transactions unique constraint
|
|
||||||
).asUpdate
|
|
||||||
case _: DbStorage.Profile.Oracle =>
|
|
||||||
throw new IllegalStateException("Oracle not supported by daml 3.0/X yet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to break up large uid-filters into batches to limit the size of sql "in-clauses".
|
|
||||||
// Fashioned to reuse lessons learned in 2.x-based DbTopologyStore
|
|
||||||
private def findTransactionsBatchingUidFilter(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
isProposal: Boolean,
|
|
||||||
types: Set[TopologyMappingX.Code],
|
|
||||||
filterUid: Option[Seq[UniqueIdentifier]],
|
|
||||||
filterNamespace: Option[Seq[Namespace]],
|
|
||||||
filterOp: Option[TopologyChangeOpX],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
def forwardBatch(filterUidsNew: Option[Seq[UniqueIdentifier]]) =
|
|
||||||
findTransactionsSingleBatch(
|
|
||||||
asOf,
|
|
||||||
asOfInclusive,
|
|
||||||
isProposal,
|
|
||||||
types,
|
|
||||||
filterUidsNew,
|
|
||||||
filterNamespace,
|
|
||||||
filterOp,
|
|
||||||
)
|
|
||||||
|
|
||||||
filterUid.map(
|
|
||||||
// Optimization: remove uid-filters made redundant by namespace filters
|
|
||||||
_.filterNot(uid => filterNamespace.exists(_.contains(uid.namespace)))
|
|
||||||
) match {
|
|
||||||
case None => forwardBatch(None)
|
|
||||||
case Some(uids) =>
|
|
||||||
MonadUtil
|
|
||||||
.batchedSequentialTraverse(
|
|
||||||
parallelism = storage.threadsAvailableForWriting,
|
|
||||||
chunkSize = maxItemsInSqlQuery,
|
|
||||||
)(uids) { batchedUidFilters => forwardBatch(Some(batchedUidFilters)).map(_.result) }
|
|
||||||
.map(StoredTopologyTransactionsX(_))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def findTransactionsSingleBatch(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
isProposal: Boolean,
|
|
||||||
types: Set[TopologyMappingX.Code],
|
|
||||||
filterUid: Option[Seq[UniqueIdentifier]],
|
|
||||||
filterNamespace: Option[Seq[Namespace]],
|
|
||||||
filterOp: Option[TopologyChangeOpX],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val hasUidFilter = filterUid.nonEmpty || filterNamespace.nonEmpty
|
|
||||||
// exit early if the caller produced an empty uid/namespace filter batch:
|
|
||||||
if (hasUidFilter && filterUid.forall(_.isEmpty) && filterNamespace.forall(_.isEmpty)) {
|
|
||||||
Future.successful(StoredTopologyTransactionsX.empty)
|
|
||||||
} else {
|
|
||||||
logger.debug(s"Querying transactions as of $asOf for types $types")
|
|
||||||
|
|
||||||
val timeRangeFilter = asOfQuery(asOf, asOfInclusive)
|
|
||||||
val isProposalFilter = sql" AND is_proposal = $isProposal"
|
|
||||||
val changeOpFilter = filterOp.fold(sql"")(op => sql" AND operation = $op")
|
|
||||||
val mappingTypeFilter = typeFilter(types)
|
|
||||||
val uidNamespaceFilter =
|
|
||||||
if (hasUidFilter) {
|
|
||||||
val namespaceFilter = filterNamespace.toList.flatMap(_.map(ns => sql"namespace = $ns"))
|
|
||||||
val uidFilter =
|
|
||||||
filterUid.toList.flatten.map(uid =>
|
|
||||||
sql"(identifier = ${uid.id} AND namespace = ${uid.namespace})"
|
|
||||||
)
|
|
||||||
sql" AND (" ++ (namespaceFilter ++ uidFilter).intercalate(sql" OR ") ++ sql")"
|
|
||||||
} else SQLActionBuilderChain(sql"")
|
|
||||||
|
|
||||||
queryForTransactions(
|
|
||||||
timeRangeFilter ++ isProposalFilter ++ changeOpFilter ++ mappingTypeFilter ++ uidNamespaceFilter,
|
|
||||||
operation = "singleBatch",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def typeFilter(types: Set[TopologyMappingX.Code]): SQLActionBuilderChain = {
|
|
||||||
if (types.isEmpty) sql""
|
|
||||||
else
|
|
||||||
sql" AND transaction_type IN (" ++ types.toSeq
|
|
||||||
.map(t => sql"$t")
|
|
||||||
.intercalate(sql", ") ++ sql")"
|
|
||||||
}
|
|
||||||
|
|
||||||
private def findAsOfExclusive(
|
|
||||||
effective: EffectiveTime,
|
|
||||||
subQuery: SQLActionBuilder,
|
|
||||||
operation: String,
|
|
||||||
)(implicit traceContext: TraceContext): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
queryForTransactions(asOfQuery(effective.value, asOfInclusive = false) ++ subQuery, operation)
|
|
||||||
.map(_.result.map(_.transaction))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def findStoredSql(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericTopologyTransactionX,
|
|
||||||
subQuery: SQLActionBuilder = sql"",
|
|
||||||
includeRejected: Boolean = false,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val mapping = transaction.mapping
|
|
||||||
queryForTransactions(
|
|
||||||
// Query for leading fields of `idx_common_topology_transactions` to enable use of this index
|
|
||||||
sql" AND transaction_type = ${mapping.code} AND namespace = ${mapping.namespace} AND identifier = ${mapping.maybeUid
|
|
||||||
.fold(String185.empty)(_.id.toLengthLimitedString)}"
|
|
||||||
++ sql" AND valid_from < $asOfExclusive"
|
|
||||||
++ sql" AND mapping_key_hash = ${mapping.uniqueKey.hash.toLengthLimitedHexString}"
|
|
||||||
++ sql" AND serial_counter = ${transaction.serial}"
|
|
||||||
++ sql" AND tx_hash = ${transaction.hash.hash.toLengthLimitedHexString}"
|
|
||||||
++ sql" AND operation = ${transaction.operation}"
|
|
||||||
++ subQuery,
|
|
||||||
includeRejected = includeRejected,
|
|
||||||
operation = "findStored",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def queryForTransactions(
|
|
||||||
subQuery: SQLActionBuilder,
|
|
||||||
operation: String,
|
|
||||||
limit: String = "",
|
|
||||||
orderBy: String = " ORDER BY id ",
|
|
||||||
includeRejected: Boolean = false,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val query =
|
|
||||||
sql"SELECT instance, sequenced, valid_from, valid_until FROM common_topology_transactions WHERE store_id = $transactionStoreIdName" ++
|
|
||||||
subQuery ++ (if (!includeRejected) sql" AND rejection_reason IS NULL"
|
|
||||||
else sql"") ++ sql" #${orderBy} #${limit}"
|
|
||||||
|
|
||||||
storage
|
|
||||||
.query(
|
|
||||||
query.as[
|
|
||||||
(
|
|
||||||
GenericSignedTopologyTransactionX,
|
|
||||||
CantonTimestamp,
|
|
||||||
CantonTimestamp,
|
|
||||||
Option[CantonTimestamp],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
s"$functionFullName-$operation",
|
|
||||||
)
|
|
||||||
.map(_.map { case (tx, sequencedTs, validFrom, validUntil) =>
|
|
||||||
StoredTopologyTransactionX(
|
|
||||||
SequencedTime(sequencedTs),
|
|
||||||
EffectiveTime(validFrom),
|
|
||||||
validUntil.map(EffectiveTime(_)),
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(StoredTopologyTransactionsX(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
override def currentDispatchingWatermark(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[CantonTimestamp]] = {
|
|
||||||
val query =
|
|
||||||
sql"SELECT watermark_ts FROM common_topology_dispatching WHERE store_id =$transactionStoreIdName"
|
|
||||||
.as[CantonTimestamp]
|
|
||||||
.headOption
|
|
||||||
storage.query(query, functionFullName)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override def updateDispatchingWatermark(timestamp: CantonTimestamp)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Unit] = {
|
|
||||||
val query = storage.profile match {
|
|
||||||
case _: DbStorage.Profile.Postgres =>
|
|
||||||
sqlu"""insert into common_topology_dispatching (store_id, watermark_ts)
|
|
||||||
VALUES ($transactionStoreIdName, $timestamp)
|
|
||||||
on conflict (store_id) do update
|
|
||||||
set
|
|
||||||
watermark_ts = $timestamp
|
|
||||||
"""
|
|
||||||
case _: DbStorage.Profile.H2 | _: DbStorage.Profile.Oracle =>
|
|
||||||
sqlu"""merge into common_topology_dispatching
|
|
||||||
using dual
|
|
||||||
on (store_id = $transactionStoreIdName)
|
|
||||||
when matched then
|
|
||||||
update set
|
|
||||||
watermark_ts = $timestamp
|
|
||||||
when not matched then
|
|
||||||
insert (store_id, watermark_ts)
|
|
||||||
values ($transactionStoreIdName, $timestamp)
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
storage.update_(query, functionFullName)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private def asOfQuery(asOf: CantonTimestamp, asOfInclusive: Boolean): SQLActionBuilder =
|
|
||||||
if (asOfInclusive)
|
|
||||||
sql" AND valid_from <= $asOf AND (valid_until is NULL OR $asOf < valid_until)"
|
|
||||||
else
|
|
||||||
sql" AND valid_from < $asOf AND (valid_until is NULL OR $asOf <= valid_until)"
|
|
||||||
|
|
||||||
private def getHeadStateQuery(
|
|
||||||
recentTimestampO: Option[CantonTimestamp]
|
|
||||||
): SQLActionBuilderChain = recentTimestampO match {
|
|
||||||
case Some(value) => asOfQuery(value, asOfInclusive = false)
|
|
||||||
case None => sql" AND valid_until is NULL"
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("com.digitalasset.canton.SlickString"))
|
|
||||||
private def getIdFilter(
|
|
||||||
idFilter: Option[String]
|
|
||||||
): SQLActionBuilderChain =
|
|
||||||
idFilter match {
|
|
||||||
case Some(value) if value.nonEmpty => sql" AND identifier like ${value + "%"}"
|
|
||||||
case _ => sql""
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("com.digitalasset.canton.SlickString"))
|
|
||||||
private def getNamespaceFilter(namespaceFilter: Option[String]): SQLActionBuilderChain =
|
|
||||||
namespaceFilter match {
|
|
||||||
case Some(value) if value.nonEmpty => sql" AND namespace LIKE ${value + "%"}"
|
|
||||||
case _ => sql""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper case class to hold StoredTopologyTransactionX-fields in update() providing umbrella
|
|
||||||
// values for all transactions.
|
|
||||||
private[db] final case class TransactionEntry(
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
validFrom: EffectiveTime,
|
|
||||||
validUntil: Option[EffectiveTime],
|
|
||||||
signedTx: GenericSignedTopologyTransactionX,
|
|
||||||
rejectionReason: Option[TopologyTransactionRejection] = None,
|
|
||||||
)
|
|
||||||
|
|
||||||
private[db] object TransactionEntry {
|
|
||||||
def fromStoredTx(stx: GenericStoredTopologyTransactionX): TransactionEntry = TransactionEntry(
|
|
||||||
stx.sequenced,
|
|
||||||
stx.validFrom,
|
|
||||||
stx.validUntil,
|
|
||||||
stx.transaction,
|
|
||||||
rejectionReason = None,
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package com.digitalasset.canton.topology.store.memory
|
||||||
|
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.LengthLimitedString.DisplayName
|
||||||
|
import com.digitalasset.canton.config.CantonRequireTypes.String255
|
||||||
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
|
import com.digitalasset.canton.topology.store.{PartyMetadata, PartyMetadataStore}
|
||||||
|
import com.digitalasset.canton.topology.{ParticipantId, PartyId}
|
||||||
|
import com.digitalasset.canton.tracing.TraceContext
|
||||||
|
|
||||||
|
import scala.collection.concurrent.TrieMap
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
class InMemoryPartyMetadataStore extends PartyMetadataStore {
|
||||||
|
|
||||||
|
private val store = TrieMap[PartyId, PartyMetadata]()
|
||||||
|
|
||||||
|
override def insertOrUpdatePartyMetadata(
|
||||||
|
partyId: PartyId,
|
||||||
|
participantId: Option[ParticipantId],
|
||||||
|
displayName: Option[DisplayName],
|
||||||
|
effectiveTimestamp: CantonTimestamp,
|
||||||
|
submissionId: String255,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = {
|
||||||
|
store
|
||||||
|
.put(
|
||||||
|
partyId,
|
||||||
|
PartyMetadata(partyId, displayName, participantId)(
|
||||||
|
effectiveTimestamp = effectiveTimestamp,
|
||||||
|
submissionId = submissionId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.discard
|
||||||
|
Future.unit
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override def metadataForParty(partyId: PartyId)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[PartyMetadata]] =
|
||||||
|
Future.successful(store.get(partyId))
|
||||||
|
|
||||||
|
override def markNotified(
|
||||||
|
metadata: PartyMetadata
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = {
|
||||||
|
store.get(metadata.partyId) match {
|
||||||
|
case Some(cur) if cur.effectiveTimestamp == metadata.effectiveTimestamp =>
|
||||||
|
store
|
||||||
|
.put(
|
||||||
|
metadata.partyId,
|
||||||
|
metadata.copy()(
|
||||||
|
effectiveTimestamp = metadata.effectiveTimestamp,
|
||||||
|
submissionId = metadata.submissionId,
|
||||||
|
notified = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.discard
|
||||||
|
case _ => ()
|
||||||
|
}
|
||||||
|
Future.unit
|
||||||
|
}
|
||||||
|
|
||||||
|
override def fetchNotNotified()(implicit traceContext: TraceContext): Future[Seq[PartyMetadata]] =
|
||||||
|
Future.successful(store.values.filterNot(_.notified).toSeq)
|
||||||
|
|
||||||
|
override def close(): Unit = ()
|
||||||
|
}
|
@ -3,82 +3,592 @@
|
|||||||
|
|
||||||
package com.digitalasset.canton.topology.store.memory
|
package com.digitalasset.canton.topology.store.memory
|
||||||
|
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.LengthLimitedString.DisplayName
|
import com.daml.nonempty.NonEmpty
|
||||||
import com.digitalasset.canton.config.CantonRequireTypes.String255
|
import com.digitalasset.canton.config.ProcessingTimeout
|
||||||
|
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||||
|
import com.digitalasset.canton.crypto.Hash
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
import com.digitalasset.canton.logging.NamedLogging
|
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
|
||||||
|
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||||
import com.digitalasset.canton.topology.*
|
import com.digitalasset.canton.topology.*
|
||||||
|
import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime}
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionX.GenericStoredTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.{
|
||||||
|
GenericStoredTopologyTransactionsX,
|
||||||
|
PositiveStoredTopologyTransactionsX,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.store.ValidatedTopologyTransactionX.GenericValidatedTopologyTransactionX
|
||||||
import com.digitalasset.canton.topology.store.*
|
import com.digitalasset.canton.topology.store.*
|
||||||
|
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
||||||
|
import com.digitalasset.canton.topology.transaction.TopologyTransactionX.{
|
||||||
|
GenericTopologyTransactionX,
|
||||||
|
TxHash,
|
||||||
|
}
|
||||||
|
import com.digitalasset.canton.topology.transaction.*
|
||||||
import com.digitalasset.canton.tracing.TraceContext
|
import com.digitalasset.canton.tracing.TraceContext
|
||||||
|
import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion}
|
||||||
|
import com.google.common.annotations.VisibleForTesting
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import scala.annotation.nowarn
|
import scala.collection.mutable
|
||||||
import scala.collection.concurrent.TrieMap
|
import scala.collection.mutable.ArrayBuffer
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.{ExecutionContext, Future, blocking}
|
||||||
|
import scala.math.Ordering.Implicits.*
|
||||||
|
|
||||||
// TODO(#15161): Rename file to InMemoryPartyMetadataStore
|
class InMemoryTopologyStoreX[+StoreId <: TopologyStoreId](
|
||||||
class InMemoryPartyMetadataStore extends PartyMetadataStore {
|
val storeId: StoreId,
|
||||||
|
val loggerFactory: NamedLoggerFactory,
|
||||||
|
override val timeouts: ProcessingTimeout,
|
||||||
|
)(implicit ec: ExecutionContext)
|
||||||
|
extends TopologyStoreX[StoreId]
|
||||||
|
with NamedLogging {
|
||||||
|
|
||||||
private val store = TrieMap[PartyId, PartyMetadata]()
|
override def onClosed(): Unit = ()
|
||||||
|
|
||||||
override def insertOrUpdatePartyMetadata(
|
private case class TopologyStoreEntry(
|
||||||
partyId: PartyId,
|
transaction: GenericSignedTopologyTransactionX,
|
||||||
participantId: Option[ParticipantId],
|
sequenced: SequencedTime,
|
||||||
displayName: Option[DisplayName],
|
from: EffectiveTime,
|
||||||
effectiveTimestamp: CantonTimestamp,
|
rejected: Option[String],
|
||||||
submissionId: String255,
|
until: Option[EffectiveTime],
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = {
|
) extends DelegatedTopologyTransactionLike[TopologyChangeOpX, TopologyMappingX] {
|
||||||
store
|
|
||||||
.put(
|
|
||||||
partyId,
|
|
||||||
PartyMetadata(partyId, displayName, participantId)(
|
|
||||||
effectiveTimestamp = effectiveTimestamp,
|
|
||||||
submissionId = submissionId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.discard
|
|
||||||
Future.unit
|
|
||||||
|
|
||||||
|
override protected def transactionLikeDelegate
|
||||||
|
: TopologyTransactionLike[TopologyChangeOpX, TopologyMappingX] = transaction
|
||||||
|
|
||||||
|
def toStoredTransaction: StoredTopologyTransactionX[TopologyChangeOpX, TopologyMappingX] =
|
||||||
|
StoredTopologyTransactionX(sequenced, from, until, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def metadataForParty(partyId: PartyId)(implicit
|
private val topologyTransactionStore = ArrayBuffer[TopologyStoreEntry]()
|
||||||
traceContext: TraceContext
|
// the unique key is defined in the database migration file for the common_topology_transactions table
|
||||||
): Future[Option[PartyMetadata]] =
|
private val topologyTransactionsStoreUniqueIndex = mutable.Set.empty[
|
||||||
Future.successful(store.get(partyId))
|
(
|
||||||
|
MappingHash,
|
||||||
|
PositiveInt,
|
||||||
|
EffectiveTime,
|
||||||
|
TopologyChangeOpX,
|
||||||
|
RepresentativeProtocolVersion[SignedTopologyTransactionX.type],
|
||||||
|
Hash,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
private val watermark = new AtomicReference[Option[CantonTimestamp]](None)
|
||||||
|
|
||||||
override def markNotified(
|
def findTransactionsByTxHash(asOfExclusive: EffectiveTime, hashes: Set[TxHash])(implicit
|
||||||
metadata: PartyMetadata
|
traceContext: TraceContext
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = {
|
): Future[Seq[GenericSignedTopologyTransactionX]] =
|
||||||
store.get(metadata.partyId) match {
|
if (hashes.isEmpty) Future.successful(Seq.empty)
|
||||||
case Some(cur) if cur.effectiveTimestamp == metadata.effectiveTimestamp =>
|
else
|
||||||
store
|
findFilter(
|
||||||
.put(
|
asOfExclusive,
|
||||||
metadata.partyId,
|
entry => hashes.contains(entry.hash),
|
||||||
metadata.copy()(
|
)
|
||||||
effectiveTimestamp = metadata.effectiveTimestamp,
|
|
||||||
submissionId = metadata.submissionId,
|
override def findProposalsByTxHash(
|
||||||
notified = true,
|
asOfExclusive: EffectiveTime,
|
||||||
),
|
hashes: NonEmpty[Set[TxHash]],
|
||||||
|
)(implicit traceContext: TraceContext): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
findFilter(
|
||||||
|
asOfExclusive,
|
||||||
|
entry => hashes.contains(entry.hash) && entry.transaction.isProposal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def findFilter(
|
||||||
|
asOfExclusive: EffectiveTime,
|
||||||
|
filter: TopologyStoreEntry => Boolean,
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]] = Future.successful {
|
||||||
|
blocking {
|
||||||
|
synchronized {
|
||||||
|
topologyTransactionStore
|
||||||
|
.filter(x =>
|
||||||
|
x.from.value < asOfExclusive.value
|
||||||
|
&& x.rejected.isEmpty
|
||||||
|
&& x.until.forall(_.value >= asOfExclusive.value)
|
||||||
|
&& filter(x)
|
||||||
)
|
)
|
||||||
.discard
|
.map(_.transaction)
|
||||||
case _ => ()
|
.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findTransactionsForMapping(
|
||||||
|
asOfExclusive: EffectiveTime,
|
||||||
|
hashes: NonEmpty[Set[MappingHash]],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
findFilter(
|
||||||
|
asOfExclusive,
|
||||||
|
entry =>
|
||||||
|
!entry.transaction.isProposal && hashes.contains(
|
||||||
|
entry.mapping.uniqueKey
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def update(
|
||||||
|
sequenced: SequencedTime,
|
||||||
|
effective: EffectiveTime,
|
||||||
|
removeMapping: Map[TopologyMappingX.MappingHash, PositiveInt],
|
||||||
|
removeTxs: Set[TopologyTransactionX.TxHash],
|
||||||
|
additions: Seq[GenericValidatedTopologyTransactionX],
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = {
|
||||||
|
blocking {
|
||||||
|
synchronized {
|
||||||
|
// transactionally
|
||||||
|
// UPDATE txs SET valid_until = effective WHERE effective < $effective AND valid_from is NULL
|
||||||
|
// AND ((mapping_key_hash IN $removeMapping AND serial_counter <= $serial) OR (tx_hash IN $removeTxs))
|
||||||
|
// INSERT IGNORE DUPLICATES (...)
|
||||||
|
topologyTransactionStore.zipWithIndex.foreach { case (tx, idx) =>
|
||||||
|
if (
|
||||||
|
tx.from.value < effective.value && tx.until.isEmpty &&
|
||||||
|
(removeMapping
|
||||||
|
.get(tx.mapping.uniqueKey)
|
||||||
|
.exists(_ >= tx.serial)
|
||||||
|
||
|
||||||
|
removeTxs.contains(tx.hash))
|
||||||
|
) {
|
||||||
|
topologyTransactionStore.update(idx, tx.copy(until = Some(effective)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
additions.foreach { tx =>
|
||||||
|
val uniqueKey = (
|
||||||
|
tx.mapping.uniqueKey,
|
||||||
|
tx.serial,
|
||||||
|
effective,
|
||||||
|
tx.operation,
|
||||||
|
tx.transaction.representativeProtocolVersion,
|
||||||
|
tx.transaction.hashOfSignatures,
|
||||||
|
)
|
||||||
|
if (topologyTransactionsStoreUniqueIndex.add(uniqueKey)) {
|
||||||
|
topologyTransactionStore.append(
|
||||||
|
TopologyStoreEntry(
|
||||||
|
tx.transaction,
|
||||||
|
sequenced,
|
||||||
|
from = effective,
|
||||||
|
rejected = tx.rejectionReason.map(_.toString),
|
||||||
|
until = Option.when(
|
||||||
|
tx.rejectionReason.nonEmpty || tx.expireImmediately
|
||||||
|
)(effective),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Future.unit
|
Future.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
override def fetchNotNotified()(implicit traceContext: TraceContext): Future[Seq[PartyMetadata]] =
|
@VisibleForTesting
|
||||||
Future.successful(store.values.filterNot(_.notified).toSeq)
|
override protected[topology] def dumpStoreContent()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val entries = blocking {
|
||||||
|
synchronized {
|
||||||
|
logger.debug(
|
||||||
|
topologyTransactionStore
|
||||||
|
.map(_.toString)
|
||||||
|
.mkString("Topology Store Content[", ", ", "]")
|
||||||
|
)
|
||||||
|
topologyTransactionStore.toSeq
|
||||||
|
|
||||||
override def close(): Unit = ()
|
}
|
||||||
}
|
}
|
||||||
|
Future.successful(
|
||||||
|
StoredTopologyTransactionsX(
|
||||||
|
entries.map(e => StoredTopologyTransactionX(e.sequenced, e.from, e.until, e.transaction))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(#15161) collapse into InMemoryTopologyStoreX
|
private def asOfFilter(
|
||||||
trait InMemoryTopologyStoreCommon[+StoreId <: TopologyStoreId] extends NamedLogging {
|
asOf: CantonTimestamp,
|
||||||
this: TopologyStoreX[StoreId] =>
|
asOfInclusive: Boolean,
|
||||||
|
): (CantonTimestamp, Option[CantonTimestamp]) => Boolean =
|
||||||
|
if (asOfInclusive) { case (validFrom, validUntil) =>
|
||||||
|
validFrom <= asOf && validUntil.forall(until => asOf < until)
|
||||||
|
}
|
||||||
|
else { case (validFrom, validUntil) =>
|
||||||
|
validFrom < asOf && validUntil.forall(until => asOf <= until)
|
||||||
|
}
|
||||||
|
|
||||||
private val watermark = new AtomicReference[Option[CantonTimestamp]](None)
|
private def filteredState(
|
||||||
|
table: Seq[TopologyStoreEntry],
|
||||||
|
filter: TopologyStoreEntry => Boolean,
|
||||||
|
includeRejected: Boolean = false,
|
||||||
|
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]] =
|
||||||
|
Future.successful(
|
||||||
|
StoredTopologyTransactionsX(
|
||||||
|
table.collect {
|
||||||
|
case entry if filter(entry) && (entry.rejected.isEmpty || includeRejected) =>
|
||||||
|
entry.toStoredTransaction
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def inspectKnownParties(
|
||||||
|
timestamp: CantonTimestamp,
|
||||||
|
filterParty: String,
|
||||||
|
filterParticipant: String,
|
||||||
|
limit: Int,
|
||||||
|
)(implicit traceContext: TraceContext): Future[Set[PartyId]] = {
|
||||||
|
val (prefixPartyIdentifier, prefixPartyNS) = UniqueIdentifier.splitFilter(filterParty)
|
||||||
|
val (prefixParticipantIdentifier, prefixParticipantNS) =
|
||||||
|
UniqueIdentifier.splitFilter(filterParticipant)
|
||||||
|
|
||||||
|
def filter(entry: TopologyStoreEntry): Boolean = {
|
||||||
|
// active
|
||||||
|
entry.from.value < timestamp && entry.until.forall(until => timestamp <= until.value) &&
|
||||||
|
// not rejected
|
||||||
|
entry.rejected.isEmpty &&
|
||||||
|
// is not a proposal
|
||||||
|
!entry.transaction.isProposal &&
|
||||||
|
// is of type Replace
|
||||||
|
entry.operation == TopologyChangeOpX.Replace &&
|
||||||
|
// matches a party to participant mapping (with appropriate filters)
|
||||||
|
(entry.mapping match {
|
||||||
|
case ptp: PartyToParticipantX =>
|
||||||
|
ptp.partyId.uid.matchesPrefixes(prefixPartyIdentifier, prefixPartyNS) &&
|
||||||
|
(filterParticipant.isEmpty ||
|
||||||
|
ptp.participants.exists(
|
||||||
|
_.participantId.uid
|
||||||
|
.matchesPrefixes(prefixParticipantIdentifier, prefixParticipantNS)
|
||||||
|
))
|
||||||
|
case cert: DomainTrustCertificateX =>
|
||||||
|
cert.participantId.adminParty.uid
|
||||||
|
.matchesPrefixes(prefixPartyIdentifier, prefixPartyNS) &&
|
||||||
|
cert.participantId.uid
|
||||||
|
.matchesPrefixes(prefixParticipantIdentifier, prefixParticipantNS)
|
||||||
|
case _ => false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val topologyStateStoreSeq = blocking(synchronized(topologyTransactionStore.toSeq))
|
||||||
|
Future.successful(
|
||||||
|
topologyStateStoreSeq
|
||||||
|
.foldLeft(Set.empty[PartyId]) {
|
||||||
|
case (acc, elem) if acc.size >= limit || !filter(elem) => acc
|
||||||
|
case (acc, elem) =>
|
||||||
|
elem.mapping.maybeUid.fold(acc)(x => acc + PartyId(x))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def inspect(
|
||||||
|
proposals: Boolean,
|
||||||
|
timeQuery: TimeQuery,
|
||||||
|
recentTimestampO: Option[CantonTimestamp],
|
||||||
|
op: Option[TopologyChangeOpX],
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
idFilter: Option[String],
|
||||||
|
namespaceFilter: Option[String],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]] = {
|
||||||
|
def mkAsOfFilter(asOf: CantonTimestamp): TopologyStoreEntry => Boolean = entry =>
|
||||||
|
asOfFilter(asOf, asOfInclusive = false)(entry.from.value, entry.until.map(_.value))
|
||||||
|
|
||||||
|
val filter1: TopologyStoreEntry => Boolean = timeQuery match {
|
||||||
|
case TimeQuery.HeadState =>
|
||||||
|
// use recent timestamp to avoid race conditions (as we are looking
|
||||||
|
// directly into the store, while the recent time still needs to propagate)
|
||||||
|
recentTimestampO.map(mkAsOfFilter).getOrElse(entry => entry.until.isEmpty)
|
||||||
|
case TimeQuery.Snapshot(asOf) => mkAsOfFilter(asOf)
|
||||||
|
case TimeQuery.Range(from, until) =>
|
||||||
|
entry =>
|
||||||
|
from.forall(ts => entry.from.value >= ts) && until.forall(ts => entry.from.value <= ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
val filter2: TopologyStoreEntry => Boolean = entry => op.forall(_ == entry.operation)
|
||||||
|
|
||||||
|
val filter3: TopologyStoreEntry => Boolean = {
|
||||||
|
idFilter match {
|
||||||
|
case Some(value) if value.nonEmpty =>
|
||||||
|
(entry: TopologyStoreEntry) =>
|
||||||
|
entry.mapping.maybeUid.exists(_.id.unwrap.startsWith(value))
|
||||||
|
case _ => _ => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val filter4: TopologyStoreEntry => Boolean = {
|
||||||
|
namespaceFilter match {
|
||||||
|
case Some(value) if value.nonEmpty =>
|
||||||
|
(entry: TopologyStoreEntry) =>
|
||||||
|
entry.mapping.namespace.fingerprint.unwrap.startsWith(value)
|
||||||
|
case _ => _ => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val filter0: TopologyStoreEntry => Boolean = entry =>
|
||||||
|
types.isEmpty || types.contains(entry.mapping.code)
|
||||||
|
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized(topologyTransactionStore.toSeq)),
|
||||||
|
entry =>
|
||||||
|
filter0(entry) && (entry.transaction.isProposal == proposals) && filter1(entry) && filter2(
|
||||||
|
entry
|
||||||
|
) && filter3(entry) && filter4(entry),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findPositiveTransactions(
|
||||||
|
asOf: CantonTimestamp,
|
||||||
|
asOfInclusive: Boolean,
|
||||||
|
isProposal: Boolean,
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
filterUid: Option[Seq[UniqueIdentifier]],
|
||||||
|
filterNamespace: Option[Seq[Namespace]],
|
||||||
|
)(implicit traceContext: TraceContext): Future[PositiveStoredTopologyTransactionsX] =
|
||||||
|
findTransactionsInStore(asOf, asOfInclusive, isProposal, types, filterUid, filterNamespace).map(
|
||||||
|
_.collectOfType[TopologyChangeOpX.Replace]
|
||||||
|
)
|
||||||
|
|
||||||
|
private def findTransactionsInStore(
|
||||||
|
asOf: CantonTimestamp,
|
||||||
|
asOfInclusive: Boolean,
|
||||||
|
isProposal: Boolean,
|
||||||
|
types: Seq[TopologyMappingX.Code],
|
||||||
|
filterUid: Option[Seq[UniqueIdentifier]],
|
||||||
|
filterNamespace: Option[Seq[Namespace]],
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
val timeFilter = asOfFilter(asOf, asOfInclusive)
|
||||||
|
def pathFilter(mapping: TopologyMappingX): Boolean = {
|
||||||
|
if (filterUid.isEmpty && filterNamespace.isEmpty)
|
||||||
|
true
|
||||||
|
else {
|
||||||
|
mapping.maybeUid.exists(uid => filterUid.exists(_.contains(uid))) ||
|
||||||
|
filterNamespace.exists(_.contains(mapping.namespace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized { topologyTransactionStore.toSeq }),
|
||||||
|
entry => {
|
||||||
|
timeFilter(entry.from.value, entry.until.map(_.value)) &&
|
||||||
|
types.contains(entry.mapping.code) &&
|
||||||
|
(pathFilter(entry.mapping)) &&
|
||||||
|
entry.transaction.isProposal == isProposal
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findFirstSequencerStateForSequencer(
|
||||||
|
sequencerId: SequencerId
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[
|
||||||
|
Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, SequencerDomainStateX]]
|
||||||
|
] = {
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized(topologyTransactionStore.toSeq)),
|
||||||
|
entry =>
|
||||||
|
!entry.transaction.isProposal &&
|
||||||
|
entry.operation == TopologyChangeOpX.Replace &&
|
||||||
|
entry.mapping
|
||||||
|
.select[SequencerDomainStateX]
|
||||||
|
.exists(m => m.allSequencers.contains(sequencerId)),
|
||||||
|
).map(
|
||||||
|
_.collectOfType[TopologyChangeOpX.Replace]
|
||||||
|
.collectOfMapping[SequencerDomainStateX]
|
||||||
|
.result
|
||||||
|
.sortBy(_.serial)
|
||||||
|
.headOption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findFirstMediatorStateForMediator(
|
||||||
|
mediatorId: MediatorId
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, MediatorDomainStateX]]] = {
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized(topologyTransactionStore.toSeq)),
|
||||||
|
entry =>
|
||||||
|
!entry.transaction.isProposal &&
|
||||||
|
entry.operation == TopologyChangeOpX.Replace &&
|
||||||
|
entry.mapping
|
||||||
|
.select[MediatorDomainStateX]
|
||||||
|
.exists(m => m.observers.contains(mediatorId) || m.active.contains(mediatorId)),
|
||||||
|
).map(
|
||||||
|
_.collectOfType[TopologyChangeOpX.Replace]
|
||||||
|
.collectOfMapping[MediatorDomainStateX]
|
||||||
|
.result
|
||||||
|
.sortBy(_.serial)
|
||||||
|
.headOption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def findFirstTrustCertificateForParticipant(
|
||||||
|
participant: ParticipantId
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[
|
||||||
|
Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, DomainTrustCertificateX]]
|
||||||
|
] = {
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized(topologyTransactionStore.toSeq)),
|
||||||
|
entry =>
|
||||||
|
!entry.transaction.isProposal &&
|
||||||
|
entry.operation == TopologyChangeOpX.Replace &&
|
||||||
|
entry.mapping
|
||||||
|
.select[DomainTrustCertificateX]
|
||||||
|
.exists(_.participantId == participant),
|
||||||
|
).map(
|
||||||
|
_.collectOfType[TopologyChangeOpX.Replace]
|
||||||
|
.collectOfMapping[DomainTrustCertificateX]
|
||||||
|
.result
|
||||||
|
.sortBy(_.serial)
|
||||||
|
.headOption
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findEssentialStateAtSequencedTime(
|
||||||
|
asOfInclusive: SequencedTime
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] = {
|
||||||
|
// asOfInclusive is the effective time of the transaction that onboarded the member.
|
||||||
|
// 1. load all transactions with a sequenced time <= asOfInclusive, including proposals
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized {
|
||||||
|
topologyTransactionStore.toSeq
|
||||||
|
}),
|
||||||
|
entry => entry.sequenced <= asOfInclusive,
|
||||||
|
).map(
|
||||||
|
// 2. transform the result such that the validUntil fields are set as they were at maxEffective time of the snapshot
|
||||||
|
_.asSnapshotAtMaxEffectiveTime
|
||||||
|
// and remove proposals that have been superseded by full authorized transactions
|
||||||
|
.retainAuthorizedHistoryAndEffectiveProposals
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** store an initial set of topology transactions as given into the store */
|
||||||
|
override def bootstrap(
|
||||||
|
snapshot: GenericStoredTopologyTransactionsX
|
||||||
|
)(implicit traceContext: TraceContext): Future[Unit] = Future {
|
||||||
|
blocking {
|
||||||
|
synchronized {
|
||||||
|
topologyTransactionStore
|
||||||
|
.appendAll(
|
||||||
|
snapshot.result.map { tx =>
|
||||||
|
TopologyStoreEntry(
|
||||||
|
tx.transaction,
|
||||||
|
tx.sequenced,
|
||||||
|
tx.validFrom,
|
||||||
|
rejected = None,
|
||||||
|
until = tx.validUntil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.discard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findUpcomingEffectiveChanges(asOfInclusive: CantonTimestamp)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Seq[TopologyStoreX.Change]] =
|
||||||
|
Future {
|
||||||
|
blocking {
|
||||||
|
synchronized {
|
||||||
|
TopologyStoreX.accumulateUpcomingEffectiveChanges(
|
||||||
|
topologyTransactionStore
|
||||||
|
.filter(_.from.value >= asOfInclusive)
|
||||||
|
.map(_.toStoredTransaction)
|
||||||
|
.toSeq
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def maxTimestamp()(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[(SequencedTime, EffectiveTime)]] = Future {
|
||||||
|
blocking {
|
||||||
|
synchronized {
|
||||||
|
topologyTransactionStore.lastOption.map(x => (x.sequenced, x.from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findDispatchingTransactionsAfter(
|
||||||
|
timestampExclusive: CantonTimestamp,
|
||||||
|
limit: Option[Int],
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] =
|
||||||
|
Future.successful(blocking(synchronized {
|
||||||
|
val selected = topologyTransactionStore
|
||||||
|
.filter(x =>
|
||||||
|
x.from.value > timestampExclusive && (!x.transaction.isProposal || x.until.isEmpty) && x.rejected.isEmpty
|
||||||
|
)
|
||||||
|
.map(_.toStoredTransaction)
|
||||||
|
.toSeq
|
||||||
|
StoredTopologyTransactionsX(limit.fold(selected)(selected.take))
|
||||||
|
}))
|
||||||
|
|
||||||
|
private def allTransactions(
|
||||||
|
includeRejected: Boolean = false
|
||||||
|
): Future[GenericStoredTopologyTransactionsX] =
|
||||||
|
filteredState(
|
||||||
|
blocking(synchronized(topologyTransactionStore.toSeq)),
|
||||||
|
_ => true,
|
||||||
|
includeRejected,
|
||||||
|
)
|
||||||
|
|
||||||
|
override def findStored(
|
||||||
|
asOfExclusive: CantonTimestamp,
|
||||||
|
transaction: GenericSignedTopologyTransactionX,
|
||||||
|
includeRejected: Boolean = false,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[GenericStoredTopologyTransactionX]] =
|
||||||
|
allTransactions(includeRejected).map(
|
||||||
|
_.result.findLast(tx => tx.hash == transaction.hash && tx.validFrom.value < asOfExclusive)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def findStoredForVersion(
|
||||||
|
asOfExclusive: CantonTimestamp,
|
||||||
|
transaction: GenericTopologyTransactionX,
|
||||||
|
protocolVersion: ProtocolVersion,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): Future[Option[GenericStoredTopologyTransactionX]] = {
|
||||||
|
val rpv = TopologyTransactionX.protocolVersionRepresentativeFor(protocolVersion)
|
||||||
|
|
||||||
|
allTransactions().map(
|
||||||
|
_.result.findLast(tx =>
|
||||||
|
tx.transaction.transaction == transaction && tx.transaction.representativeProtocolVersion == rpv && tx.validFrom.value < asOfExclusive
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def findParticipantOnboardingTransactions(
|
||||||
|
participantId: ParticipantId,
|
||||||
|
domainId: DomainId,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): FutureUnlessShutdown[Seq[GenericSignedTopologyTransactionX]] = {
|
||||||
|
val res = blocking(synchronized {
|
||||||
|
topologyTransactionStore.filter(x =>
|
||||||
|
!x.transaction.isProposal && TopologyStoreX.initialParticipantDispatchingSet.contains(
|
||||||
|
x.mapping.code
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
FutureUnlessShutdown.pure(
|
||||||
|
TopologyStoreX.filterInitialParticipantDispatchingTransactions(
|
||||||
|
participantId,
|
||||||
|
domainId,
|
||||||
|
res.map(_.toStoredTransaction).toSeq,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@nowarn("cat=unused")
|
|
||||||
override def currentDispatchingWatermark(implicit
|
override def currentDispatchingWatermark(implicit
|
||||||
traceContext: TraceContext
|
traceContext: TraceContext
|
||||||
): Future[Option[CantonTimestamp]] =
|
): Future[Option[CantonTimestamp]] =
|
||||||
@ -96,5 +606,4 @@ trait InMemoryTopologyStoreCommon[+StoreId <: TopologyStoreId] extends NamedLogg
|
|||||||
}
|
}
|
||||||
Future.unit
|
Future.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,591 +0,0 @@
|
|||||||
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package com.digitalasset.canton.topology.store.memory
|
|
||||||
|
|
||||||
import com.daml.nonempty.NonEmpty
|
|
||||||
import com.digitalasset.canton.config.ProcessingTimeout
|
|
||||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
|
||||||
import com.digitalasset.canton.crypto.Hash
|
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
|
||||||
import com.digitalasset.canton.lifecycle.FutureUnlessShutdown
|
|
||||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
|
||||||
import com.digitalasset.canton.topology.*
|
|
||||||
import com.digitalasset.canton.topology.processing.{EffectiveTime, SequencedTime}
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionX.GenericStoredTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.{
|
|
||||||
GenericStoredTopologyTransactionsX,
|
|
||||||
PositiveStoredTopologyTransactionsX,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.store.ValidatedTopologyTransactionX.GenericValidatedTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.store.*
|
|
||||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyMappingX.MappingHash
|
|
||||||
import com.digitalasset.canton.topology.transaction.TopologyTransactionX.{
|
|
||||||
GenericTopologyTransactionX,
|
|
||||||
TxHash,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.topology.transaction.*
|
|
||||||
import com.digitalasset.canton.tracing.TraceContext
|
|
||||||
import com.digitalasset.canton.version.{ProtocolVersion, RepresentativeProtocolVersion}
|
|
||||||
import com.google.common.annotations.VisibleForTesting
|
|
||||||
|
|
||||||
import scala.collection.mutable
|
|
||||||
import scala.collection.mutable.ArrayBuffer
|
|
||||||
import scala.concurrent.{ExecutionContext, Future, blocking}
|
|
||||||
import scala.math.Ordering.Implicits.*
|
|
||||||
|
|
||||||
class InMemoryTopologyStoreX[+StoreId <: TopologyStoreId](
|
|
||||||
val storeId: StoreId,
|
|
||||||
val loggerFactory: NamedLoggerFactory,
|
|
||||||
override val timeouts: ProcessingTimeout,
|
|
||||||
)(implicit ec: ExecutionContext)
|
|
||||||
extends TopologyStoreX[StoreId]
|
|
||||||
with InMemoryTopologyStoreCommon[StoreId]
|
|
||||||
with NamedLogging {
|
|
||||||
|
|
||||||
override def onClosed(): Unit = ()
|
|
||||||
|
|
||||||
private case class TopologyStoreEntry(
|
|
||||||
transaction: GenericSignedTopologyTransactionX,
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
from: EffectiveTime,
|
|
||||||
rejected: Option[String],
|
|
||||||
until: Option[EffectiveTime],
|
|
||||||
) extends DelegatedTopologyTransactionLike[TopologyChangeOpX, TopologyMappingX] {
|
|
||||||
|
|
||||||
override protected def transactionLikeDelegate
|
|
||||||
: TopologyTransactionLike[TopologyChangeOpX, TopologyMappingX] = transaction
|
|
||||||
|
|
||||||
def toStoredTransaction: StoredTopologyTransactionX[TopologyChangeOpX, TopologyMappingX] =
|
|
||||||
StoredTopologyTransactionX(sequenced, from, until, transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val topologyTransactionStore = ArrayBuffer[TopologyStoreEntry]()
|
|
||||||
// the unique key is defined in the database migration file for the common_topology_transactions table
|
|
||||||
private val topologyTransactionsStoreUniqueIndex = mutable.Set.empty[
|
|
||||||
(
|
|
||||||
MappingHash,
|
|
||||||
PositiveInt,
|
|
||||||
EffectiveTime,
|
|
||||||
TopologyChangeOpX,
|
|
||||||
RepresentativeProtocolVersion[SignedTopologyTransactionX.type],
|
|
||||||
Hash,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
def findTransactionsByTxHash(asOfExclusive: EffectiveTime, hashes: Set[TxHash])(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]] =
|
|
||||||
if (hashes.isEmpty) Future.successful(Seq.empty)
|
|
||||||
else
|
|
||||||
findFilter(
|
|
||||||
asOfExclusive,
|
|
||||||
entry => hashes.contains(entry.hash),
|
|
||||||
)
|
|
||||||
|
|
||||||
override def findProposalsByTxHash(
|
|
||||||
asOfExclusive: EffectiveTime,
|
|
||||||
hashes: NonEmpty[Set[TxHash]],
|
|
||||||
)(implicit traceContext: TraceContext): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
findFilter(
|
|
||||||
asOfExclusive,
|
|
||||||
entry => hashes.contains(entry.hash) && entry.transaction.isProposal,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def findFilter(
|
|
||||||
asOfExclusive: EffectiveTime,
|
|
||||||
filter: TopologyStoreEntry => Boolean,
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]] = Future.successful {
|
|
||||||
blocking {
|
|
||||||
synchronized {
|
|
||||||
topologyTransactionStore
|
|
||||||
.filter(x =>
|
|
||||||
x.from.value < asOfExclusive.value
|
|
||||||
&& x.rejected.isEmpty
|
|
||||||
&& x.until.forall(_.value >= asOfExclusive.value)
|
|
||||||
&& filter(x)
|
|
||||||
)
|
|
||||||
.map(_.transaction)
|
|
||||||
.toSeq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findTransactionsForMapping(
|
|
||||||
asOfExclusive: EffectiveTime,
|
|
||||||
hashes: NonEmpty[Set[MappingHash]],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
findFilter(
|
|
||||||
asOfExclusive,
|
|
||||||
entry =>
|
|
||||||
!entry.transaction.isProposal && hashes.contains(
|
|
||||||
entry.mapping.uniqueKey
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def update(
|
|
||||||
sequenced: SequencedTime,
|
|
||||||
effective: EffectiveTime,
|
|
||||||
removeMapping: Map[TopologyMappingX.MappingHash, PositiveInt],
|
|
||||||
removeTxs: Set[TopologyTransactionX.TxHash],
|
|
||||||
additions: Seq[GenericValidatedTopologyTransactionX],
|
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = {
|
|
||||||
blocking {
|
|
||||||
synchronized {
|
|
||||||
// transactionally
|
|
||||||
// UPDATE txs SET valid_until = effective WHERE effective < $effective AND valid_from is NULL
|
|
||||||
// AND ((mapping_key_hash IN $removeMapping AND serial_counter <= $serial) OR (tx_hash IN $removeTxs))
|
|
||||||
// INSERT IGNORE DUPLICATES (...)
|
|
||||||
topologyTransactionStore.zipWithIndex.foreach { case (tx, idx) =>
|
|
||||||
if (
|
|
||||||
tx.from.value < effective.value && tx.until.isEmpty &&
|
|
||||||
(removeMapping
|
|
||||||
.get(tx.mapping.uniqueKey)
|
|
||||||
.exists(_ >= tx.serial)
|
|
||||||
||
|
|
||||||
removeTxs.contains(tx.hash))
|
|
||||||
) {
|
|
||||||
topologyTransactionStore.update(idx, tx.copy(until = Some(effective)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
additions.foreach { tx =>
|
|
||||||
val uniqueKey = (
|
|
||||||
tx.mapping.uniqueKey,
|
|
||||||
tx.serial,
|
|
||||||
effective,
|
|
||||||
tx.operation,
|
|
||||||
tx.transaction.representativeProtocolVersion,
|
|
||||||
tx.transaction.hashOfSignatures,
|
|
||||||
)
|
|
||||||
if (topologyTransactionsStoreUniqueIndex.add(uniqueKey)) {
|
|
||||||
topologyTransactionStore.append(
|
|
||||||
TopologyStoreEntry(
|
|
||||||
tx.transaction,
|
|
||||||
sequenced,
|
|
||||||
from = effective,
|
|
||||||
rejected = tx.rejectionReason.map(_.toString),
|
|
||||||
until = Option.when(
|
|
||||||
tx.rejectionReason.nonEmpty || tx.expireImmediately
|
|
||||||
)(effective),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Future.unit
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
override protected[topology] def dumpStoreContent()(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val entries = blocking {
|
|
||||||
synchronized {
|
|
||||||
logger.debug(
|
|
||||||
topologyTransactionStore
|
|
||||||
.map(_.toString)
|
|
||||||
.mkString("Topology Store Content[", ", ", "]")
|
|
||||||
)
|
|
||||||
topologyTransactionStore.toSeq
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Future.successful(
|
|
||||||
StoredTopologyTransactionsX(
|
|
||||||
entries.map(e => StoredTopologyTransactionX(e.sequenced, e.from, e.until, e.transaction))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def asOfFilter(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
): (CantonTimestamp, Option[CantonTimestamp]) => Boolean =
|
|
||||||
if (asOfInclusive) { case (validFrom, validUntil) =>
|
|
||||||
validFrom <= asOf && validUntil.forall(until => asOf < until)
|
|
||||||
}
|
|
||||||
else { case (validFrom, validUntil) =>
|
|
||||||
validFrom < asOf && validUntil.forall(until => asOf <= until)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def filteredState(
|
|
||||||
table: Seq[TopologyStoreEntry],
|
|
||||||
filter: TopologyStoreEntry => Boolean,
|
|
||||||
includeRejected: Boolean = false,
|
|
||||||
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]] =
|
|
||||||
Future.successful(
|
|
||||||
StoredTopologyTransactionsX(
|
|
||||||
table.collect {
|
|
||||||
case entry if filter(entry) && (entry.rejected.isEmpty || includeRejected) =>
|
|
||||||
entry.toStoredTransaction
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def inspectKnownParties(
|
|
||||||
timestamp: CantonTimestamp,
|
|
||||||
filterParty: String,
|
|
||||||
filterParticipant: String,
|
|
||||||
limit: Int,
|
|
||||||
)(implicit traceContext: TraceContext): Future[Set[PartyId]] = {
|
|
||||||
val (prefixPartyIdentifier, prefixPartyNS) = UniqueIdentifier.splitFilter(filterParty)
|
|
||||||
val (prefixParticipantIdentifier, prefixParticipantNS) =
|
|
||||||
UniqueIdentifier.splitFilter(filterParticipant)
|
|
||||||
|
|
||||||
def filter(entry: TopologyStoreEntry): Boolean = {
|
|
||||||
// active
|
|
||||||
entry.from.value < timestamp && entry.until.forall(until => timestamp <= until.value) &&
|
|
||||||
// not rejected
|
|
||||||
entry.rejected.isEmpty &&
|
|
||||||
// is not a proposal
|
|
||||||
!entry.transaction.isProposal &&
|
|
||||||
// is of type Replace
|
|
||||||
entry.operation == TopologyChangeOpX.Replace &&
|
|
||||||
// matches a party to participant mapping (with appropriate filters)
|
|
||||||
(entry.mapping match {
|
|
||||||
case ptp: PartyToParticipantX =>
|
|
||||||
ptp.partyId.uid.matchesPrefixes(prefixPartyIdentifier, prefixPartyNS) &&
|
|
||||||
(filterParticipant.isEmpty ||
|
|
||||||
ptp.participants.exists(
|
|
||||||
_.participantId.uid
|
|
||||||
.matchesPrefixes(prefixParticipantIdentifier, prefixParticipantNS)
|
|
||||||
))
|
|
||||||
case cert: DomainTrustCertificateX =>
|
|
||||||
cert.participantId.adminParty.uid
|
|
||||||
.matchesPrefixes(prefixPartyIdentifier, prefixPartyNS) &&
|
|
||||||
cert.participantId.uid
|
|
||||||
.matchesPrefixes(prefixParticipantIdentifier, prefixParticipantNS)
|
|
||||||
case _ => false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val topologyStateStoreSeq = blocking(synchronized(topologyTransactionStore.toSeq))
|
|
||||||
Future.successful(
|
|
||||||
topologyStateStoreSeq
|
|
||||||
.foldLeft(Set.empty[PartyId]) {
|
|
||||||
case (acc, elem) if acc.size >= limit || !filter(elem) => acc
|
|
||||||
case (acc, elem) =>
|
|
||||||
elem.mapping.maybeUid.fold(acc)(x => acc + PartyId(x))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def inspect(
|
|
||||||
proposals: Boolean,
|
|
||||||
timeQuery: TimeQuery,
|
|
||||||
recentTimestampO: Option[CantonTimestamp],
|
|
||||||
op: Option[TopologyChangeOpX],
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
idFilter: Option[String],
|
|
||||||
namespaceFilter: Option[String],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[StoredTopologyTransactionsX[TopologyChangeOpX, TopologyMappingX]] = {
|
|
||||||
def mkAsOfFilter(asOf: CantonTimestamp): TopologyStoreEntry => Boolean = entry =>
|
|
||||||
asOfFilter(asOf, asOfInclusive = false)(entry.from.value, entry.until.map(_.value))
|
|
||||||
|
|
||||||
val filter1: TopologyStoreEntry => Boolean = timeQuery match {
|
|
||||||
case TimeQuery.HeadState =>
|
|
||||||
// use recent timestamp to avoid race conditions (as we are looking
|
|
||||||
// directly into the store, while the recent time still needs to propagate)
|
|
||||||
recentTimestampO.map(mkAsOfFilter).getOrElse(entry => entry.until.isEmpty)
|
|
||||||
case TimeQuery.Snapshot(asOf) => mkAsOfFilter(asOf)
|
|
||||||
case TimeQuery.Range(from, until) =>
|
|
||||||
entry =>
|
|
||||||
from.forall(ts => entry.from.value >= ts) && until.forall(ts => entry.from.value <= ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
val filter2: TopologyStoreEntry => Boolean = entry => op.forall(_ == entry.operation)
|
|
||||||
|
|
||||||
val filter3: TopologyStoreEntry => Boolean = {
|
|
||||||
idFilter match {
|
|
||||||
case Some(value) if value.nonEmpty =>
|
|
||||||
(entry: TopologyStoreEntry) =>
|
|
||||||
entry.mapping.maybeUid.exists(_.id.unwrap.startsWith(value))
|
|
||||||
case _ => _ => true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filter4: TopologyStoreEntry => Boolean = {
|
|
||||||
namespaceFilter match {
|
|
||||||
case Some(value) if value.nonEmpty =>
|
|
||||||
(entry: TopologyStoreEntry) =>
|
|
||||||
entry.mapping.namespace.fingerprint.unwrap.startsWith(value)
|
|
||||||
case _ => _ => true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filter0: TopologyStoreEntry => Boolean = entry =>
|
|
||||||
types.isEmpty || types.contains(entry.mapping.code)
|
|
||||||
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized(topologyTransactionStore.toSeq)),
|
|
||||||
entry =>
|
|
||||||
filter0(entry) && (entry.transaction.isProposal == proposals) && filter1(entry) && filter2(
|
|
||||||
entry
|
|
||||||
) && filter3(entry) && filter4(entry),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findPositiveTransactions(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
isProposal: Boolean,
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
filterUid: Option[Seq[UniqueIdentifier]],
|
|
||||||
filterNamespace: Option[Seq[Namespace]],
|
|
||||||
)(implicit traceContext: TraceContext): Future[PositiveStoredTopologyTransactionsX] =
|
|
||||||
findTransactionsInStore(asOf, asOfInclusive, isProposal, types, filterUid, filterNamespace).map(
|
|
||||||
_.collectOfType[TopologyChangeOpX.Replace]
|
|
||||||
)
|
|
||||||
|
|
||||||
private def findTransactionsInStore(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
asOfInclusive: Boolean,
|
|
||||||
isProposal: Boolean,
|
|
||||||
types: Seq[TopologyMappingX.Code],
|
|
||||||
filterUid: Option[Seq[UniqueIdentifier]],
|
|
||||||
filterNamespace: Option[Seq[Namespace]],
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
val timeFilter = asOfFilter(asOf, asOfInclusive)
|
|
||||||
def pathFilter(mapping: TopologyMappingX): Boolean = {
|
|
||||||
if (filterUid.isEmpty && filterNamespace.isEmpty)
|
|
||||||
true
|
|
||||||
else {
|
|
||||||
mapping.maybeUid.exists(uid => filterUid.exists(_.contains(uid))) ||
|
|
||||||
filterNamespace.exists(_.contains(mapping.namespace))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized { topologyTransactionStore.toSeq }),
|
|
||||||
entry => {
|
|
||||||
timeFilter(entry.from.value, entry.until.map(_.value)) &&
|
|
||||||
types.contains(entry.mapping.code) &&
|
|
||||||
(pathFilter(entry.mapping)) &&
|
|
||||||
entry.transaction.isProposal == isProposal
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findFirstSequencerStateForSequencer(
|
|
||||||
sequencerId: SequencerId
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[
|
|
||||||
Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, SequencerDomainStateX]]
|
|
||||||
] = {
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized(topologyTransactionStore.toSeq)),
|
|
||||||
entry =>
|
|
||||||
!entry.transaction.isProposal &&
|
|
||||||
entry.operation == TopologyChangeOpX.Replace &&
|
|
||||||
entry.mapping
|
|
||||||
.select[SequencerDomainStateX]
|
|
||||||
.exists(m => m.allSequencers.contains(sequencerId)),
|
|
||||||
).map(
|
|
||||||
_.collectOfType[TopologyChangeOpX.Replace]
|
|
||||||
.collectOfMapping[SequencerDomainStateX]
|
|
||||||
.result
|
|
||||||
.sortBy(_.serial)
|
|
||||||
.headOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findFirstMediatorStateForMediator(
|
|
||||||
mediatorId: MediatorId
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, MediatorDomainStateX]]] = {
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized(topologyTransactionStore.toSeq)),
|
|
||||||
entry =>
|
|
||||||
!entry.transaction.isProposal &&
|
|
||||||
entry.operation == TopologyChangeOpX.Replace &&
|
|
||||||
entry.mapping
|
|
||||||
.select[MediatorDomainStateX]
|
|
||||||
.exists(m => m.observers.contains(mediatorId) || m.active.contains(mediatorId)),
|
|
||||||
).map(
|
|
||||||
_.collectOfType[TopologyChangeOpX.Replace]
|
|
||||||
.collectOfMapping[MediatorDomainStateX]
|
|
||||||
.result
|
|
||||||
.sortBy(_.serial)
|
|
||||||
.headOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def findFirstTrustCertificateForParticipant(
|
|
||||||
participant: ParticipantId
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[
|
|
||||||
Option[StoredTopologyTransactionX[TopologyChangeOpX.Replace, DomainTrustCertificateX]]
|
|
||||||
] = {
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized(topologyTransactionStore.toSeq)),
|
|
||||||
entry =>
|
|
||||||
!entry.transaction.isProposal &&
|
|
||||||
entry.operation == TopologyChangeOpX.Replace &&
|
|
||||||
entry.mapping
|
|
||||||
.select[DomainTrustCertificateX]
|
|
||||||
.exists(_.participantId == participant),
|
|
||||||
).map(
|
|
||||||
_.collectOfType[TopologyChangeOpX.Replace]
|
|
||||||
.collectOfMapping[DomainTrustCertificateX]
|
|
||||||
.result
|
|
||||||
.sortBy(_.serial)
|
|
||||||
.headOption
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findEssentialStateAtSequencedTime(
|
|
||||||
asOfInclusive: SequencedTime
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] = {
|
|
||||||
// asOfInclusive is the effective time of the transaction that onboarded the member.
|
|
||||||
// 1. load all transactions with a sequenced time <= asOfInclusive, including proposals
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized {
|
|
||||||
topologyTransactionStore.toSeq
|
|
||||||
}),
|
|
||||||
entry => entry.sequenced <= asOfInclusive,
|
|
||||||
).map(
|
|
||||||
// 2. transform the result such that the validUntil fields are set as they were at maxEffective time of the snapshot
|
|
||||||
_.asSnapshotAtMaxEffectiveTime
|
|
||||||
// and remove proposals that have been superseded by full authorized transactions
|
|
||||||
.retainAuthorizedHistoryAndEffectiveProposals
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** store an initial set of topology transactions as given into the store */
|
|
||||||
override def bootstrap(
|
|
||||||
snapshot: GenericStoredTopologyTransactionsX
|
|
||||||
)(implicit traceContext: TraceContext): Future[Unit] = Future {
|
|
||||||
blocking {
|
|
||||||
synchronized {
|
|
||||||
topologyTransactionStore
|
|
||||||
.appendAll(
|
|
||||||
snapshot.result.map { tx =>
|
|
||||||
TopologyStoreEntry(
|
|
||||||
tx.transaction,
|
|
||||||
tx.sequenced,
|
|
||||||
tx.validFrom,
|
|
||||||
rejected = None,
|
|
||||||
until = tx.validUntil,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.discard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findUpcomingEffectiveChanges(asOfInclusive: CantonTimestamp)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Seq[TopologyStoreX.Change]] =
|
|
||||||
Future {
|
|
||||||
blocking {
|
|
||||||
synchronized {
|
|
||||||
TopologyStoreX.accumulateUpcomingEffectiveChanges(
|
|
||||||
topologyTransactionStore
|
|
||||||
.filter(_.from.value >= asOfInclusive)
|
|
||||||
.map(_.toStoredTransaction)
|
|
||||||
.toSeq
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def maxTimestamp()(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[(SequencedTime, EffectiveTime)]] = Future {
|
|
||||||
blocking {
|
|
||||||
synchronized {
|
|
||||||
topologyTransactionStore.lastOption.map(x => (x.sequenced, x.from))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findDispatchingTransactionsAfter(
|
|
||||||
timestampExclusive: CantonTimestamp,
|
|
||||||
limit: Option[Int],
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] =
|
|
||||||
Future.successful(blocking(synchronized {
|
|
||||||
val selected = topologyTransactionStore
|
|
||||||
.filter(x =>
|
|
||||||
x.from.value > timestampExclusive && (!x.transaction.isProposal || x.until.isEmpty) && x.rejected.isEmpty
|
|
||||||
)
|
|
||||||
.map(_.toStoredTransaction)
|
|
||||||
.toSeq
|
|
||||||
StoredTopologyTransactionsX(limit.fold(selected)(selected.take))
|
|
||||||
}))
|
|
||||||
|
|
||||||
private def allTransactions(
|
|
||||||
includeRejected: Boolean = false
|
|
||||||
): Future[GenericStoredTopologyTransactionsX] =
|
|
||||||
filteredState(
|
|
||||||
blocking(synchronized(topologyTransactionStore.toSeq)),
|
|
||||||
_ => true,
|
|
||||||
includeRejected,
|
|
||||||
)
|
|
||||||
|
|
||||||
override def findStored(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericSignedTopologyTransactionX,
|
|
||||||
includeRejected: Boolean = false,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[GenericStoredTopologyTransactionX]] =
|
|
||||||
allTransactions(includeRejected).map(
|
|
||||||
_.result.findLast(tx => tx.hash == transaction.hash && tx.validFrom.value < asOfExclusive)
|
|
||||||
)
|
|
||||||
|
|
||||||
override def findStoredForVersion(
|
|
||||||
asOfExclusive: CantonTimestamp,
|
|
||||||
transaction: GenericTopologyTransactionX,
|
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): Future[Option[GenericStoredTopologyTransactionX]] = {
|
|
||||||
val rpv = TopologyTransactionX.protocolVersionRepresentativeFor(protocolVersion)
|
|
||||||
|
|
||||||
allTransactions().map(
|
|
||||||
_.result.findLast(tx =>
|
|
||||||
tx.transaction.transaction == transaction && tx.transaction.representativeProtocolVersion == rpv && tx.validFrom.value < asOfExclusive
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override def findParticipantOnboardingTransactions(
|
|
||||||
participantId: ParticipantId,
|
|
||||||
domainId: DomainId,
|
|
||||||
)(implicit
|
|
||||||
traceContext: TraceContext
|
|
||||||
): FutureUnlessShutdown[Seq[GenericSignedTopologyTransactionX]] = {
|
|
||||||
val res = blocking(synchronized {
|
|
||||||
topologyTransactionStore.filter(x =>
|
|
||||||
!x.transaction.isProposal && TopologyStoreX.initialParticipantDispatchingSet.contains(
|
|
||||||
x.mapping.code
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
FutureUnlessShutdown.pure(
|
|
||||||
TopologyStoreX.filterInitialParticipantDispatchingTransactions(
|
|
||||||
participantId,
|
|
||||||
domainId,
|
|
||||||
res.map(_.toStoredTransaction).toSeq,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -4,7 +4,6 @@
|
|||||||
package com.digitalasset.canton.domain.sequencing.sequencer.block
|
package com.digitalasset.canton.domain.sequencing.sequencer.block
|
||||||
|
|
||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
import cats.syntax.bifunctor.*
|
|
||||||
import cats.syntax.either.*
|
import cats.syntax.either.*
|
||||||
import com.digitalasset.canton.SequencerCounter
|
import com.digitalasset.canton.SequencerCounter
|
||||||
import com.digitalasset.canton.concurrent.FutureSupervisor
|
import com.digitalasset.canton.concurrent.FutureSupervisor
|
||||||
|
@ -31,6 +31,13 @@ service PackageManagementService {
|
|||||||
// was already uploaded before.
|
// was already uploaded before.
|
||||||
// - Respond with a gRPC error
|
// - Respond with a gRPC error
|
||||||
rpc UploadDarFile(UploadDarFileRequest) returns (UploadDarFileResponse);
|
rpc UploadDarFile(UploadDarFileRequest) returns (UploadDarFileResponse);
|
||||||
|
|
||||||
|
// Performs the same checks that UploadDarFile call perform, but doesn't
|
||||||
|
// upload the DAR and does not make it available on the whole ledger.
|
||||||
|
// This call may:
|
||||||
|
// - Succeed if the package is valid
|
||||||
|
// - Respond with a gRPC error if the package is not valid
|
||||||
|
rpc ValidateDarFile(ValidateDarFileRequest) returns (ValidateDarFileResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListKnownPackagesRequest {}
|
message ListKnownPackagesRequest {}
|
||||||
@ -75,5 +82,21 @@ message UploadDarFileRequest {
|
|||||||
string submission_id = 2;
|
string submission_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// An empty message that is received when the upload operation succeeded.
|
// A message that is received when the upload operation succeeded.
|
||||||
message UploadDarFileResponse {}
|
message UploadDarFileResponse {}
|
||||||
|
|
||||||
|
// Performs the same checks that UploadDarFileRequest would perform, but doesn't
|
||||||
|
// upload the DAR.
|
||||||
|
message ValidateDarFileRequest {
|
||||||
|
// Contains a Daml archive DAR file, which in turn is a jar like zipped
|
||||||
|
// container for ``daml_lf`` archives. See further details in
|
||||||
|
// ``daml_lf.proto``.
|
||||||
|
// Required
|
||||||
|
bytes dar_file = 1;
|
||||||
|
|
||||||
|
// Unique submission identifier.
|
||||||
|
// Optional, defaults to a random identifier.
|
||||||
|
string submission_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ValidateDarFileResponse {}
|
||||||
|
@ -28,6 +28,9 @@ final class PackageManagementServiceAuthorization(
|
|||||||
override def uploadDarFile(request: UploadDarFileRequest): Future[UploadDarFileResponse] =
|
override def uploadDarFile(request: UploadDarFileRequest): Future[UploadDarFileResponse] =
|
||||||
authorizer.requireAdminClaims(service.uploadDarFile)(request)
|
authorizer.requireAdminClaims(service.uploadDarFile)(request)
|
||||||
|
|
||||||
|
override def validateDarFile(request: ValidateDarFileRequest): Future[ValidateDarFileResponse] =
|
||||||
|
authorizer.requireAdminClaims(service.validateDarFile)(request)
|
||||||
|
|
||||||
override def bindService(): ServerServiceDefinition =
|
override def bindService(): ServerServiceDefinition =
|
||||||
PackageManagementServiceGrpc.bindService(this, executionContext)
|
PackageManagementServiceGrpc.bindService(this, executionContext)
|
||||||
|
|
||||||
|
@ -223,8 +223,6 @@ final class CommandsValidator(
|
|||||||
} yield Submitters(actAs, readAs)
|
} yield Submitters(actAs, readAs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(i12279): Address usage of deprecated class DeduplicationTime
|
|
||||||
|
|
||||||
/** We validate only using current time because we set the currentTime as submitTime so no need to check both
|
/** We validate only using current time because we set the currentTime as submitTime so no need to check both
|
||||||
*/
|
*/
|
||||||
def validateDeduplicationPeriod(
|
def validateDeduplicationPeriod(
|
||||||
|
@ -19,12 +19,4 @@ trait IndexCompletionsService extends LedgerEndService {
|
|||||||
applicationId: Ref.ApplicationId,
|
applicationId: Ref.ApplicationId,
|
||||||
parties: Set[Ref.Party],
|
parties: Set[Ref.Party],
|
||||||
)(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed]
|
)(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed]
|
||||||
|
|
||||||
// TODO(i12282): Remove, as possible. This is solely serving KV Deduplication Offset -> Duration conversion
|
|
||||||
def getCompletions(
|
|
||||||
startExclusive: ParticipantOffset,
|
|
||||||
endInclusive: ParticipantOffset,
|
|
||||||
applicationId: Ref.ApplicationId,
|
|
||||||
parties: Set[Ref.Party],
|
|
||||||
)(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed]
|
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import com.daml.ledger.api.v2.update_service.{
|
|||||||
GetUpdatesResponse,
|
GetUpdatesResponse,
|
||||||
}
|
}
|
||||||
import com.daml.lf.data.Ref
|
import com.daml.lf.data.Ref
|
||||||
import com.daml.lf.data.Ref.{ApplicationId, Party}
|
import com.daml.lf.data.Ref.ApplicationId
|
||||||
import com.daml.lf.data.Time.Timestamp
|
import com.daml.lf.data.Time.Timestamp
|
||||||
import com.daml.lf.transaction.GlobalKey
|
import com.daml.lf.transaction.GlobalKey
|
||||||
import com.daml.lf.value.Value
|
import com.daml.lf.value.Value
|
||||||
@ -173,17 +173,6 @@ final class TimedIndexService(delegate: IndexService, metrics: Metrics) extends
|
|||||||
delegate.prune(pruneUpToInclusive, pruneAllDivulgedContracts, incompletReassignmentOffsets),
|
delegate.prune(pruneUpToInclusive, pruneAllDivulgedContracts, incompletReassignmentOffsets),
|
||||||
)
|
)
|
||||||
|
|
||||||
override def getCompletions(
|
|
||||||
startExclusive: ParticipantOffset,
|
|
||||||
endInclusive: ParticipantOffset,
|
|
||||||
applicationId: Ref.ApplicationId,
|
|
||||||
parties: Set[Party],
|
|
||||||
)(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed] =
|
|
||||||
Timed.source(
|
|
||||||
metrics.services.index.getCompletionsLimited,
|
|
||||||
delegate.getCompletions(startExclusive, endInclusive, applicationId, parties),
|
|
||||||
)
|
|
||||||
|
|
||||||
override def currentHealth(): HealthStatus =
|
override def currentHealth(): HealthStatus =
|
||||||
delegate.currentHealth()
|
delegate.currentHealth()
|
||||||
|
|
||||||
|
@ -136,6 +136,15 @@ private[apiserver] final class ApiPackageManagementService private (
|
|||||||
} yield dar
|
} yield dar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def validateDarFile(request: ValidateDarFileRequest): Future[ValidateDarFileResponse] = {
|
||||||
|
val submissionId = submissionIdGenerator(request.submissionId)
|
||||||
|
LoggingContextWithTrace.withEnrichedLoggingContext(telemetry)(
|
||||||
|
logging.submissionId(submissionId)
|
||||||
|
) { implicit loggingContext: LoggingContextWithTrace =>
|
||||||
|
decodeAndValidate(request.darFile).map((_: Dar[Archive]) => ValidateDarFileResponse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override def uploadDarFile(request: UploadDarFileRequest): Future[UploadDarFileResponse] = {
|
override def uploadDarFile(request: UploadDarFileRequest): Future[UploadDarFileResponse] = {
|
||||||
val submissionId = submissionIdGenerator(request.submissionId)
|
val submissionId = submissionIdGenerator(request.submissionId)
|
||||||
LoggingContextWithTrace.withEnrichedLoggingContext(telemetry)(
|
LoggingContextWithTrace.withEnrichedLoggingContext(telemetry)(
|
||||||
|
@ -235,24 +235,6 @@ private[index] class IndexServiceImpl(
|
|||||||
}
|
}
|
||||||
.buffered(metrics.index.completionsBufferSize, LedgerApiStreamsBufferSize)
|
.buffered(metrics.index.completionsBufferSize, LedgerApiStreamsBufferSize)
|
||||||
|
|
||||||
override def getCompletions(
|
|
||||||
startExclusive: ParticipantOffset,
|
|
||||||
endInclusive: ParticipantOffset,
|
|
||||||
applicationId: Ref.ApplicationId,
|
|
||||||
parties: Set[Ref.Party],
|
|
||||||
)(implicit loggingContext: LoggingContextWithTrace): Source[CompletionStreamResponse, NotUsed] =
|
|
||||||
between(startExclusive, Some(endInclusive)) { (start, end) =>
|
|
||||||
dispatcher()
|
|
||||||
.startingAt(
|
|
||||||
start.getOrElse(Offset.beforeBegin),
|
|
||||||
RangeSource(commandCompletionsReader.getCommandCompletions(_, _, applicationId, parties)),
|
|
||||||
end,
|
|
||||||
)
|
|
||||||
.mapError(shutdownError)
|
|
||||||
.map(_._2)
|
|
||||||
}
|
|
||||||
.buffered(metrics.index.completionsBufferSize, LedgerApiStreamsBufferSize)
|
|
||||||
|
|
||||||
override def getActiveContracts(
|
override def getActiveContracts(
|
||||||
transactionFilter: TransactionFilter,
|
transactionFilter: TransactionFilter,
|
||||||
verbose: Boolean,
|
verbose: Boolean,
|
||||||
|
@ -12,6 +12,8 @@ import com.daml.ledger.api.testing.utils.PekkoBeforeAndAfterAll
|
|||||||
import com.daml.ledger.api.v2.admin.package_management_service.{
|
import com.daml.ledger.api.v2.admin.package_management_service.{
|
||||||
PackageManagementServiceGrpc,
|
PackageManagementServiceGrpc,
|
||||||
UploadDarFileRequest,
|
UploadDarFileRequest,
|
||||||
|
ValidateDarFileRequest,
|
||||||
|
ValidateDarFileResponse,
|
||||||
}
|
}
|
||||||
import com.daml.lf.archive.testing.Encode
|
import com.daml.lf.archive.testing.Encode
|
||||||
import com.daml.lf.archive.{Dar, GenDarReader}
|
import com.daml.lf.archive.{Dar, GenDarReader}
|
||||||
@ -118,6 +120,15 @@ class ApiPackageManagementServiceSpec
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"validate a dar" in {
|
||||||
|
val apiService = createApiService()
|
||||||
|
apiService
|
||||||
|
.validateDarFile(ValidateDarFileRequest(ByteString.EMPTY, aSubmissionId))
|
||||||
|
.map { case ValidateDarFileResponse() =>
|
||||||
|
succeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"close while uploading dar" in {
|
"close while uploading dar" in {
|
||||||
val writeService = mock[state.WritePackagesService]
|
val writeService = mock[state.WritePackagesService]
|
||||||
when(
|
when(
|
||||||
|
@ -8,6 +8,7 @@ import com.daml.ledger.api.v2.admin.package_management_service.{
|
|||||||
ListKnownPackagesRequest,
|
ListKnownPackagesRequest,
|
||||||
PackageDetails,
|
PackageDetails,
|
||||||
UploadDarFileRequest,
|
UploadDarFileRequest,
|
||||||
|
ValidateDarFileRequest,
|
||||||
}
|
}
|
||||||
import com.digitalasset.canton.ledger.client.LedgerClient
|
import com.digitalasset.canton.ledger.client.LedgerClient
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
@ -30,10 +31,21 @@ final class PackageManagementClient(service: PackageManagementServiceStub)(impli
|
|||||||
.listKnownPackages(PackageManagementClient.listKnownPackagesRequest)
|
.listKnownPackages(PackageManagementClient.listKnownPackagesRequest)
|
||||||
.map(_.packageDetails)
|
.map(_.packageDetails)
|
||||||
|
|
||||||
def uploadDarFile(darFile: ByteString, token: Option[String] = None): Future[Unit] =
|
def uploadDarFile(
|
||||||
|
darFile: ByteString,
|
||||||
|
token: Option[String] = None,
|
||||||
|
): Future[Unit] =
|
||||||
LedgerClient
|
LedgerClient
|
||||||
.stub(service, token)
|
.stub(service, token)
|
||||||
.uploadDarFile(UploadDarFileRequest(darFile))
|
.uploadDarFile(UploadDarFileRequest(darFile))
|
||||||
.map(_ => ())
|
.map(_ => ())
|
||||||
|
|
||||||
|
def validateDarFile(
|
||||||
|
darFile: ByteString,
|
||||||
|
token: Option[String] = None,
|
||||||
|
): Future[Unit] =
|
||||||
|
LedgerClient
|
||||||
|
.stub(service, token)
|
||||||
|
.validateDarFile(ValidateDarFileRequest(darFile))
|
||||||
|
.map(_ => ())
|
||||||
}
|
}
|
||||||
|
@ -308,7 +308,7 @@ object PackageServiceErrors extends PackageServiceErrorGroup {
|
|||||||
ErrorCategory.InvalidIndependentOfSystemState,
|
ErrorCategory.InvalidIndependentOfSystemState,
|
||||||
) {
|
) {
|
||||||
final case class Error(
|
final case class Error(
|
||||||
uploadedPackage: Ref.PackageId,
|
uploadedPackageId: Ref.PackageId,
|
||||||
existingPackage: Ref.PackageId,
|
existingPackage: Ref.PackageId,
|
||||||
packageVersion: Ref.PackageVersion,
|
packageVersion: Ref.PackageVersion,
|
||||||
)(implicit
|
)(implicit
|
||||||
@ -316,7 +316,7 @@ object PackageServiceErrors extends PackageServiceErrorGroup {
|
|||||||
) extends DamlError(
|
) extends DamlError(
|
||||||
cause = "A DAR with the same version number has previously been uploaded.",
|
cause = "A DAR with the same version number has previously been uploaded.",
|
||||||
extraContext = Map(
|
extraContext = Map(
|
||||||
"uploadedPackage" -> uploadedPackage,
|
"uploadedPackageId" -> uploadedPackageId,
|
||||||
"existingPackage" -> existingPackage,
|
"existingPackage" -> existingPackage,
|
||||||
"packageVersion" -> packageVersion.toString,
|
"packageVersion" -> packageVersion.toString,
|
||||||
),
|
),
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
package com.digitalasset.canton.http
|
package com.digitalasset.canton.http
|
||||||
|
|
||||||
|
import com.digitalasset.canton.ledger.api.tls.TlsConfiguration
|
||||||
import com.digitalasset.canton.pureconfigutils.HttpServerConfig
|
import com.digitalasset.canton.pureconfigutils.HttpServerConfig
|
||||||
|
|
||||||
final case class HttpApiConfig(
|
final case class HttpApiConfig(
|
||||||
@ -14,10 +15,11 @@ final case class HttpApiConfig(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO(#13303) Use directly instead of using JsonApiConfig as indirection
|
// TODO(#13303) Use directly instead of using JsonApiConfig as indirection
|
||||||
def toConfig: JsonApiConfig = {
|
def toConfig(tls: Option[TlsConfiguration]): JsonApiConfig = {
|
||||||
JsonApiConfig(
|
JsonApiConfig(
|
||||||
address = server.address,
|
address = server.address,
|
||||||
httpPort = server.port,
|
httpPort = server.port,
|
||||||
|
httpsConfiguration = tls,
|
||||||
portFile = server.portFile,
|
portFile = server.portFile,
|
||||||
staticContentConfig = staticContent,
|
staticContentConfig = staticContent,
|
||||||
allowNonHttps = allowInsecureTokens,
|
allowNonHttps = allowInsecureTokens,
|
||||||
|
@ -3,21 +3,15 @@
|
|||||||
|
|
||||||
package com.digitalasset.canton.http
|
package com.digitalasset.canton.http
|
||||||
|
|
||||||
import org.apache.pekko.actor.ActorSystem
|
|
||||||
import org.apache.pekko.http.scaladsl.Http
|
|
||||||
import org.apache.pekko.http.scaladsl.Http.ServerBinding
|
|
||||||
import org.apache.pekko.http.scaladsl.server.Route
|
|
||||||
import org.apache.pekko.http.scaladsl.settings.ServerSettings
|
|
||||||
import org.apache.pekko.stream.Materializer
|
|
||||||
import com.daml.grpc.adapter.ExecutionSequencerFactory
|
import com.daml.grpc.adapter.ExecutionSequencerFactory
|
||||||
import com.daml.jwt.JwtDecoder
|
import com.daml.jwt.JwtDecoder
|
||||||
import com.daml.jwt.domain.Jwt
|
import com.daml.jwt.domain.Jwt
|
||||||
import com.digitalasset.canton.ledger.api.refinements.ApiTypes.ApplicationId
|
|
||||||
import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner}
|
import com.daml.ledger.resources.{Resource, ResourceContext, ResourceOwner}
|
||||||
import com.daml.logging.LoggingContextOf
|
import com.daml.logging.LoggingContextOf
|
||||||
import com.daml.metrics.pekkohttp.HttpMetricsInterceptor
|
import com.daml.metrics.pekkohttp.HttpMetricsInterceptor
|
||||||
import com.daml.ports.{Port, PortFiles}
|
import com.daml.ports.{Port, PortFiles}
|
||||||
import com.digitalasset.canton.concurrent.DirectExecutionContext
|
import com.digitalasset.canton.concurrent.DirectExecutionContext
|
||||||
|
import com.digitalasset.canton.http.HttpService.buildKeyStore
|
||||||
import com.digitalasset.canton.http.json.{
|
import com.digitalasset.canton.http.json.{
|
||||||
ApiValueToJsValueConverter,
|
ApiValueToJsValueConverter,
|
||||||
DomainJsonDecoder,
|
DomainJsonDecoder,
|
||||||
@ -28,23 +22,34 @@ import com.digitalasset.canton.http.metrics.HttpApiMetrics
|
|||||||
import com.digitalasset.canton.http.util.ApiValueToLfValueConverter
|
import com.digitalasset.canton.http.util.ApiValueToLfValueConverter
|
||||||
import com.digitalasset.canton.http.util.FutureUtil.*
|
import com.digitalasset.canton.http.util.FutureUtil.*
|
||||||
import com.digitalasset.canton.http.util.Logging.InstanceUUID
|
import com.digitalasset.canton.http.util.Logging.InstanceUUID
|
||||||
|
import com.digitalasset.canton.ledger.api.refinements.ApiTypes.ApplicationId
|
||||||
|
import com.digitalasset.canton.ledger.api.tls.TlsConfiguration
|
||||||
|
import com.digitalasset.canton.ledger.client.LedgerClient as DamlLedgerClient
|
||||||
import com.digitalasset.canton.ledger.client.configuration.{
|
import com.digitalasset.canton.ledger.client.configuration.{
|
||||||
CommandClientConfiguration,
|
CommandClientConfiguration,
|
||||||
LedgerClientConfiguration,
|
LedgerClientConfiguration,
|
||||||
}
|
}
|
||||||
import com.digitalasset.canton.ledger.client.services.pkg.PackageClient
|
import com.digitalasset.canton.ledger.client.services.pkg.PackageClient
|
||||||
import com.digitalasset.canton.ledger.client.LedgerClient as DamlLedgerClient
|
|
||||||
import com.digitalasset.canton.ledger.service.LedgerReader
|
import com.digitalasset.canton.ledger.service.LedgerReader
|
||||||
import com.digitalasset.canton.ledger.service.LedgerReader.PackageStore
|
import com.digitalasset.canton.ledger.service.LedgerReader.PackageStore
|
||||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||||
import com.digitalasset.canton.tracing.NoTracing
|
import com.digitalasset.canton.tracing.NoTracing
|
||||||
import io.grpc.Channel
|
import io.grpc.Channel
|
||||||
import io.grpc.health.v1.health.{HealthCheckRequest, HealthGrpc}
|
import io.grpc.health.v1.health.{HealthCheckRequest, HealthGrpc}
|
||||||
|
import org.apache.pekko.actor.ActorSystem
|
||||||
|
import org.apache.pekko.http.scaladsl.Http.ServerBinding
|
||||||
|
import org.apache.pekko.http.scaladsl.server.Route
|
||||||
|
import org.apache.pekko.http.scaladsl.settings.ServerSettings
|
||||||
|
import org.apache.pekko.http.scaladsl.{ConnectionContext, Http, HttpsConnectionContext}
|
||||||
|
import org.apache.pekko.stream.Materializer
|
||||||
import scalaz.*
|
import scalaz.*
|
||||||
import scalaz.Scalaz.*
|
import scalaz.Scalaz.*
|
||||||
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.{Files, Path}
|
||||||
|
import java.security.{Key, KeyStore}
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.Using
|
||||||
|
|
||||||
class HttpService(
|
class HttpService(
|
||||||
startSettings: StartSettings,
|
startSettings: StartSettings,
|
||||||
@ -151,7 +156,7 @@ class HttpService(
|
|||||||
(encoder, decoder) = HttpService.buildJsonCodecs(packageService)
|
(encoder, decoder) = HttpService.buildJsonCodecs(packageService)
|
||||||
|
|
||||||
jsonEndpoints = new Endpoints(
|
jsonEndpoints = new Endpoints(
|
||||||
allowNonHttps,
|
startSettings.httpsConfiguration.isEmpty,
|
||||||
HttpService.decodeJwt,
|
HttpService.decodeJwt,
|
||||||
commandService,
|
commandService,
|
||||||
contractsService,
|
contractsService,
|
||||||
@ -197,12 +202,18 @@ class HttpService(
|
|||||||
EndpointsCompanion.notFound(logger),
|
EndpointsCompanion.notFound(logger),
|
||||||
)
|
)
|
||||||
|
|
||||||
binding <- liftET[HttpService.Error](
|
binding <- liftET[HttpService.Error] {
|
||||||
Http()
|
val serverBuilder = Http()
|
||||||
.newServerAt(address, httpPort.getOrElse(0))
|
.newServerAt(address, httpPort.getOrElse(0))
|
||||||
.withSettings(settings)
|
.withSettings(settings)
|
||||||
|
|
||||||
|
httpsConfiguration
|
||||||
|
.fold(serverBuilder) { config =>
|
||||||
|
logger.info(s"Enabling HTTPS with $config")
|
||||||
|
serverBuilder.enableHttps(HttpService.httpsConnectionContext(config))
|
||||||
|
}
|
||||||
.bind(allEndpoints)
|
.bind(allEndpoints)
|
||||||
)
|
}
|
||||||
|
|
||||||
_ <- either(portFile.cata(f => HttpService.createPortFile(f, binding), \/-(()))): ET[Unit]
|
_ <- either(portFile.cata(f => HttpService.createPortFile(f, binding), \/-(()))): ET[Unit]
|
||||||
|
|
||||||
@ -216,6 +227,7 @@ class HttpService(
|
|||||||
logger.info(s"Stopping JSON API server..., ${lc.makeString}")
|
logger.info(s"Stopping JSON API server..., ${lc.makeString}")
|
||||||
binding.unbind().void
|
binding.unbind().void
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object HttpService {
|
object HttpService {
|
||||||
@ -277,5 +289,65 @@ object HttpService {
|
|||||||
PortFiles.write(file, Port(binding.localAddress.getPort)).liftErr(Error.apply)
|
PortFiles.write(file, Port(binding.localAddress.getPort)).liftErr(Error.apply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def buildSSLContext(config: TlsConfiguration): SSLContext = {
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
|
||||||
|
|
||||||
|
val keyStore = buildKeyStore(config)
|
||||||
|
|
||||||
|
val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
|
||||||
|
keyManagerFactory.init(keyStore, null)
|
||||||
|
|
||||||
|
val trustManagerFactory = TrustManagerFactory.getInstance("SunX509")
|
||||||
|
trustManagerFactory.init(keyStore)
|
||||||
|
|
||||||
|
val context = SSLContext.getInstance("TLS")
|
||||||
|
context.init(
|
||||||
|
keyManagerFactory.getKeyManagers,
|
||||||
|
trustManagerFactory.getTrustManagers,
|
||||||
|
new SecureRandom,
|
||||||
|
)
|
||||||
|
context
|
||||||
|
}
|
||||||
|
|
||||||
|
private def httpsConnectionContext(config: TlsConfiguration): HttpsConnectionContext =
|
||||||
|
ConnectionContext.httpsServer(buildSSLContext(config))
|
||||||
|
|
||||||
|
private def buildKeyStore(config: TlsConfiguration): KeyStore = buildKeyStore(
|
||||||
|
config.certChainFile.get.toPath,
|
||||||
|
config.privateKeyFile.get.toPath,
|
||||||
|
config.trustCollectionFile.get.toPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
private def buildKeyStore(certFile: Path, privateKeyFile: Path, caCertFile: Path): KeyStore = {
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
val alias = "key" // This can be anything as long as it's consistent.
|
||||||
|
|
||||||
|
val cf = CertificateFactory.getInstance("X.509")
|
||||||
|
val cert = Using.resource(Files.newInputStream(certFile)) { cf.generateCertificate(_) }
|
||||||
|
val caCert = Using.resource(Files.newInputStream(caCertFile)) { cf.generateCertificate(_) }
|
||||||
|
val privateKey = loadPrivateKey(privateKeyFile)
|
||||||
|
|
||||||
|
val keyStore = KeyStore.getInstance("PKCS12")
|
||||||
|
keyStore.load(null)
|
||||||
|
keyStore.setCertificateEntry(alias, cert)
|
||||||
|
keyStore.setCertificateEntry(alias, caCert)
|
||||||
|
keyStore.setKeyEntry(alias, privateKey, null, Array(cert, caCert))
|
||||||
|
keyStore.setCertificateEntry("trusted-ca", caCert)
|
||||||
|
keyStore
|
||||||
|
}
|
||||||
|
|
||||||
|
private def loadPrivateKey(pkRsaPemFile: Path): Key = {
|
||||||
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||||
|
import org.bouncycastle.openssl.PEMParser
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
|
||||||
|
|
||||||
|
Using.resource(Files.newBufferedReader(pkRsaPemFile)) { reader =>
|
||||||
|
val pemParser = new PEMParser(reader)
|
||||||
|
val pkInfo = PrivateKeyInfo.getInstance(pemParser.readObject())
|
||||||
|
new JcaPEMKeyConverter().getPrivateKey(pkInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final case class Error(message: String)
|
final case class Error(message: String)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ package com.digitalasset.canton.http
|
|||||||
|
|
||||||
import org.apache.pekko.stream.ThrottleMode
|
import org.apache.pekko.stream.ThrottleMode
|
||||||
import com.digitalasset.canton.http.WebsocketConfig as WSC
|
import com.digitalasset.canton.http.WebsocketConfig as WSC
|
||||||
|
import com.digitalasset.canton.ledger.api.tls.TlsConfiguration
|
||||||
import scalaz.Show
|
import scalaz.Show
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -15,13 +16,14 @@ import scala.concurrent.duration.*
|
|||||||
// users should extend StartSettings or DefaultStartSettings themselves
|
// users should extend StartSettings or DefaultStartSettings themselves
|
||||||
// TODO(#13303): Move to LocalParticipantConfig
|
// TODO(#13303): Move to LocalParticipantConfig
|
||||||
final case class JsonApiConfig(
|
final case class JsonApiConfig(
|
||||||
address: String = com.digitalasset.canton.cliopts.Http.defaultAddress,
|
address: String = com.digitalasset.canton.cliopts.Http.defaultAddress,
|
||||||
httpPort: Option[Int] = None,
|
httpPort: Option[Int] = None,
|
||||||
portFile: Option[Path] = None,
|
portFile: Option[Path] = None,
|
||||||
staticContentConfig: Option[StaticContentConfig] = None,
|
httpsConfiguration: Option[TlsConfiguration] = None,
|
||||||
allowNonHttps: Boolean = false,
|
staticContentConfig: Option[StaticContentConfig] = None,
|
||||||
wsConfig: Option[WebsocketConfig] = None,
|
allowNonHttps: Boolean = false,
|
||||||
debugLoggingOfHttpBodies: Boolean = false,
|
wsConfig: Option[WebsocketConfig] = None,
|
||||||
|
debugLoggingOfHttpBodies: Boolean = false,
|
||||||
) extends StartSettings
|
) extends StartSettings
|
||||||
|
|
||||||
object JsonApiConfig {
|
object JsonApiConfig {
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
package com.digitalasset.canton.http
|
package com.digitalasset.canton.http
|
||||||
|
|
||||||
|
import com.digitalasset.canton.ledger.api.tls.TlsConfiguration
|
||||||
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
// defined separately from Config so
|
// defined separately from Config so
|
||||||
@ -12,8 +14,8 @@ trait StartSettings {
|
|||||||
val address: String
|
val address: String
|
||||||
val httpPort: Option[Int]
|
val httpPort: Option[Int]
|
||||||
val portFile: Option[Path]
|
val portFile: Option[Path]
|
||||||
|
val httpsConfiguration:Option[TlsConfiguration]
|
||||||
val wsConfig: Option[WebsocketConfig]
|
val wsConfig: Option[WebsocketConfig]
|
||||||
val allowNonHttps: Boolean
|
|
||||||
val staticContentConfig: Option[StaticContentConfig]
|
val staticContentConfig: Option[StaticContentConfig]
|
||||||
val debugLoggingOfHttpBodies: Boolean
|
val debugLoggingOfHttpBodies: Boolean
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ private[http] final class RouteSetup(
|
|||||||
inputJsVal(req).flatMap(x => withJwtPayload[JsValue, P](x).leftMap(it => it: Error))
|
inputJsVal(req).flatMap(x => withJwtPayload[JsValue, P](x).leftMap(it => it: Error))
|
||||||
|
|
||||||
def withJwtPayload[A, P](fa: (Jwt, A))(implicit
|
def withJwtPayload[A, P](fa: (Jwt, A))(implicit
|
||||||
createFromUserToken: CreateFromUserToken[P],
|
createFromUserToken: CreateFromUserToken[P]
|
||||||
): EitherT[Future, Error, (Jwt, P, A)] =
|
): EitherT[Future, Error, (Jwt, P, A)] =
|
||||||
decodeAndParsePayload[P](fa._1, decodeJwt, userManagementClient).map(t2 =>
|
decodeAndParsePayload[P](fa._1, decodeJwt, userManagementClient).map(t2 =>
|
||||||
(t2._1, t2._2, fa._2)
|
(t2._1, t2._2, fa._2)
|
||||||
@ -155,10 +155,12 @@ private[http] final class RouteSetup(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def isHttps(req: HttpRequest): Boolean = req.uri.scheme == "https"
|
||||||
|
|
||||||
private[this] def ensureHttpsForwarded(req: HttpRequest)(implicit
|
private[this] def ensureHttpsForwarded(req: HttpRequest)(implicit
|
||||||
lc: LoggingContextOf[InstanceUUID with RequestID]
|
lc: LoggingContextOf[InstanceUUID with RequestID]
|
||||||
): Unauthorized \/ Unit =
|
): Unauthorized \/ Unit =
|
||||||
if (allowNonHttps || isForwardedForHttps(req.headers)) \/-(())
|
if (allowNonHttps || isForwardedForHttps(req.headers) || isHttps(req)) \/-(())
|
||||||
else {
|
else {
|
||||||
logger.warn(s"$nonHttpsErrorMessage, ${lc.makeString}")
|
logger.warn(s"$nonHttpsErrorMessage, ${lc.makeString}")
|
||||||
\/-(())
|
\/-(())
|
||||||
|
@ -137,7 +137,14 @@ class CantonLedgerApiServerFactory(
|
|||||||
.initialize(
|
.initialize(
|
||||||
CantonLedgerApiServerWrapper.Config(
|
CantonLedgerApiServerWrapper.Config(
|
||||||
serverConfig = config.ledgerApi,
|
serverConfig = config.ledgerApi,
|
||||||
jsonApiConfig = config.httpLedgerApiExperimental.map(_.toConfig),
|
jsonApiConfig = config.httpLedgerApiExperimental.map(
|
||||||
|
_.toConfig(
|
||||||
|
config.ledgerApi.tls
|
||||||
|
.map(
|
||||||
|
LedgerApiServerConfig.ledgerApiServerTlsConfigFromCantonServerConfig
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
indexerConfig = parameters.ledgerApiServerParameters.indexer,
|
indexerConfig = parameters.ledgerApiServerParameters.indexer,
|
||||||
indexerHaConfig = indexerHaConfig,
|
indexerHaConfig = indexerHaConfig,
|
||||||
participantId = participantId,
|
participantId = participantId,
|
||||||
|
@ -6,6 +6,7 @@ package com.digitalasset.canton.participant.admin
|
|||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
import cats.implicits.toBifunctorOps
|
import cats.implicits.toBifunctorOps
|
||||||
import cats.syntax.either.*
|
import cats.syntax.either.*
|
||||||
|
import cats.syntax.functor.*
|
||||||
import cats.syntax.functorFilter.*
|
import cats.syntax.functorFilter.*
|
||||||
import cats.syntax.parallel.*
|
import cats.syntax.parallel.*
|
||||||
import com.daml.daml_lf_dev.DamlLf
|
import com.daml.daml_lf_dev.DamlLf
|
||||||
@ -64,6 +65,12 @@ trait DarService {
|
|||||||
vetAllPackages: Boolean,
|
vetAllPackages: Boolean,
|
||||||
synchronizeVetting: Boolean,
|
synchronizeVetting: Boolean,
|
||||||
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash]
|
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash]
|
||||||
|
|
||||||
|
def validateByteString(
|
||||||
|
payload: ByteString,
|
||||||
|
filename: String,
|
||||||
|
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash]
|
||||||
|
|
||||||
def getDar(hash: Hash)(implicit traceContext: TraceContext): Future[Option[PackageService.Dar]]
|
def getDar(hash: Hash)(implicit traceContext: TraceContext): Future[Option[PackageService.Dar]]
|
||||||
def listDars(limit: Option[Int])(implicit
|
def listDars(limit: Option[Int])(implicit
|
||||||
traceContext: TraceContext
|
traceContext: TraceContext
|
||||||
@ -308,13 +315,24 @@ class PackageService(
|
|||||||
filename: String,
|
filename: String,
|
||||||
vetAllPackages: Boolean,
|
vetAllPackages: Boolean,
|
||||||
synchronizeVetting: Boolean,
|
synchronizeVetting: Boolean,
|
||||||
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash] =
|
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash] = {
|
||||||
appendDar(
|
val darName = PathUtils.getFilenameWithoutExtension(Paths.get(filename).getFileName)
|
||||||
payload,
|
for {
|
||||||
PathUtils.getFilenameWithoutExtension(Paths.get(filename).getFileName),
|
// Validate the packages before storing them in the DAR store or the package store
|
||||||
vetAllPackages,
|
res <- validateByteStringToDar(payload, darName)
|
||||||
synchronizeVetting,
|
(hash, lengthValidatedName, dar) = res
|
||||||
)
|
_ <- storeValidatedPackagesAndSyncEvent(
|
||||||
|
dar.all,
|
||||||
|
lengthValidatedName.asString1GB,
|
||||||
|
LedgerSubmissionId.assertFromString(UUID.randomUUID().toString),
|
||||||
|
Some(
|
||||||
|
PackageService.Dar(DarDescriptor(hash, lengthValidatedName), payload.toByteArray)
|
||||||
|
),
|
||||||
|
vetAllPackages = vetAllPackages,
|
||||||
|
synchronizeVetting = synchronizeVetting,
|
||||||
|
)
|
||||||
|
} yield hash
|
||||||
|
}
|
||||||
|
|
||||||
private def catchUpstreamErrors[E](
|
private def catchUpstreamErrors[E](
|
||||||
attempt: Either[LfArchiveError, E]
|
attempt: Either[LfArchiveError, E]
|
||||||
@ -337,15 +355,21 @@ class PackageService(
|
|||||||
Left(PackageServiceErrors.InternalError.Unhandled(e))
|
Left(PackageServiceErrors.InternalError.Unhandled(e))
|
||||||
})
|
})
|
||||||
|
|
||||||
private def appendDar(
|
def validateByteString(
|
||||||
payload: ByteString,
|
payload: ByteString,
|
||||||
darName: String,
|
darName: String,
|
||||||
vetAllPackages: Boolean,
|
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash] =
|
||||||
synchronizeVetting: Boolean,
|
validateByteStringToDar(payload, darName).map(_._1)
|
||||||
)(implicit traceContext: TraceContext): EitherT[FutureUnlessShutdown, DamlError, Hash] = {
|
|
||||||
|
private def validateByteStringToDar(
|
||||||
|
payload: ByteString,
|
||||||
|
darName: String,
|
||||||
|
)(implicit
|
||||||
|
traceContext: TraceContext
|
||||||
|
): EitherT[FutureUnlessShutdown, DamlError, (Hash, String255, archive.Dar[DamlLf.Archive])] = {
|
||||||
val hash = hashOps.digest(HashPurpose.DarIdentifier, payload)
|
val hash = hashOps.digest(HashPurpose.DarIdentifier, payload)
|
||||||
val stream = new ZipInputStream(payload.newInput())
|
val stream = new ZipInputStream(payload.newInput())
|
||||||
val ret: EitherT[FutureUnlessShutdown, DamlError, Hash] = for {
|
val ret = for {
|
||||||
lengthValidatedName <- EitherT
|
lengthValidatedName <- EitherT
|
||||||
.fromEither[FutureUnlessShutdown](
|
.fromEither[FutureUnlessShutdown](
|
||||||
String255.create(darName, Some("DAR file name"))
|
String255.create(darName, Some("DAR file name"))
|
||||||
@ -353,20 +377,8 @@ class PackageService(
|
|||||||
.leftMap(PackageServiceErrors.Reading.InvalidDarFileName.Error(_))
|
.leftMap(PackageServiceErrors.Reading.InvalidDarFileName.Error(_))
|
||||||
dar <- catchUpstreamErrors(DarParser.readArchive(darName, stream))
|
dar <- catchUpstreamErrors(DarParser.readArchive(darName, stream))
|
||||||
.mapK(FutureUnlessShutdown.outcomeK)
|
.mapK(FutureUnlessShutdown.outcomeK)
|
||||||
// Validate the packages before storing them in the DAR store or the package store
|
|
||||||
_ <- validateArchives(dar).mapK(FutureUnlessShutdown.outcomeK)
|
_ <- validateArchives(dar).mapK(FutureUnlessShutdown.outcomeK)
|
||||||
_ <- storeValidatedPackagesAndSyncEvent(
|
} yield (hash, lengthValidatedName, dar)
|
||||||
dar.all,
|
|
||||||
lengthValidatedName.asString1GB,
|
|
||||||
LedgerSubmissionId.assertFromString(UUID.randomUUID().toString),
|
|
||||||
Some(
|
|
||||||
PackageService.Dar(DarDescriptor(hash, lengthValidatedName), payload.toByteArray)
|
|
||||||
),
|
|
||||||
vetAllPackages = vetAllPackages,
|
|
||||||
synchronizeVetting = synchronizeVetting,
|
|
||||||
)
|
|
||||||
|
|
||||||
} yield hash
|
|
||||||
ret.transform { res =>
|
ret.transform { res =>
|
||||||
stream.close()
|
stream.close()
|
||||||
res
|
res
|
||||||
@ -394,7 +406,7 @@ class PackageService(
|
|||||||
|
|
||||||
private def validateArchives(archives: archive.Dar[DamlLf.Archive])(implicit
|
private def validateArchives(archives: archive.Dar[DamlLf.Archive])(implicit
|
||||||
traceContext: TraceContext
|
traceContext: TraceContext
|
||||||
): EitherT[Future, DamlError, Unit] =
|
): EitherT[Future, DamlError, PackageId] =
|
||||||
for {
|
for {
|
||||||
mainPackage <- catchUpstreamErrors(Decode.decodeArchive(archives.main))
|
mainPackage <- catchUpstreamErrors(Decode.decodeArchive(archives.main))
|
||||||
dependencies <- archives.dependencies
|
dependencies <- archives.dependencies
|
||||||
@ -417,7 +429,7 @@ class PackageService(
|
|||||||
LoggingContextWithTrace(loggerFactory)
|
LoggingContextWithTrace(loggerFactory)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} yield ()
|
} yield mainPackage._1
|
||||||
|
|
||||||
def vetPackages(packages: Seq[PackageId], syncVetting: Boolean)(implicit
|
def vetPackages(packages: Seq[PackageId], syncVetting: Boolean)(implicit
|
||||||
traceContext: TraceContext
|
traceContext: TraceContext
|
||||||
|
@ -39,18 +39,35 @@ class GrpcPackageService(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def validateDar(request: ValidateDarRequest): Future[ValidateDarResponse] = {
|
||||||
|
implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext
|
||||||
|
val ret =
|
||||||
|
service
|
||||||
|
.validateByteString(
|
||||||
|
request.data,
|
||||||
|
request.filename,
|
||||||
|
)
|
||||||
|
.map((hash: Hash) => ValidateDarResponse(hash.toHexString))
|
||||||
|
EitherTUtil.toFuture(
|
||||||
|
ret
|
||||||
|
.leftMap(ErrorCode.asGrpcError)
|
||||||
|
.onShutdown(Left(GrpcErrors.AbortedDueToShutdown.Error().asGrpcError))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override def uploadDar(request: UploadDarRequest): Future[UploadDarResponse] = {
|
override def uploadDar(request: UploadDarRequest): Future[UploadDarResponse] = {
|
||||||
implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext
|
implicit val traceContext: TraceContext = TraceContextGrpc.fromGrpcContext
|
||||||
val ret = for {
|
val ret =
|
||||||
hash <- service.appendDarFromByteString(
|
for {
|
||||||
request.data,
|
hash <- service.appendDarFromByteString(
|
||||||
request.filename,
|
request.data,
|
||||||
request.vetAllPackages,
|
request.filename,
|
||||||
request.synchronizeVetting,
|
request.vetAllPackages,
|
||||||
|
request.synchronizeVetting,
|
||||||
|
)
|
||||||
|
} yield UploadDarResponse(
|
||||||
|
UploadDarResponse.Value.Success(UploadDarResponse.Success(hash.toHexString))
|
||||||
)
|
)
|
||||||
} yield UploadDarResponse(
|
|
||||||
UploadDarResponse.Value.Success(UploadDarResponse.Success(hash.toHexString))
|
|
||||||
)
|
|
||||||
EitherTUtil.toFuture(
|
EitherTUtil.toFuture(
|
||||||
ret
|
ret
|
||||||
.leftMap(ErrorCode.asGrpcError)
|
.leftMap(ErrorCode.asGrpcError)
|
||||||
|
@ -1342,10 +1342,8 @@ class CantonSyncService(
|
|||||||
partyNotifier,
|
partyNotifier,
|
||||||
missingKeysAlerter,
|
missingKeysAlerter,
|
||||||
domainHandle.topologyClient,
|
domainHandle.topologyClient,
|
||||||
domainCrypto,
|
|
||||||
trafficStateController,
|
trafficStateController,
|
||||||
ephemeral.recordOrderPublisher,
|
ephemeral.recordOrderPublisher,
|
||||||
domainHandle.staticParameters.protocolVersion,
|
|
||||||
parameters.useNewTrafficControl,
|
parameters.useNewTrafficControl,
|
||||||
),
|
),
|
||||||
missingKeysAlerter,
|
missingKeysAlerter,
|
||||||
|
@ -20,10 +20,7 @@ import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
|||||||
import com.digitalasset.canton.participant.ParticipantNodeParameters
|
import com.digitalasset.canton.participant.ParticipantNodeParameters
|
||||||
import com.digitalasset.canton.participant.domain.{DomainAliasResolution, DomainRegistryError}
|
import com.digitalasset.canton.participant.domain.{DomainAliasResolution, DomainRegistryError}
|
||||||
import com.digitalasset.canton.participant.store.*
|
import com.digitalasset.canton.participant.store.*
|
||||||
import com.digitalasset.canton.participant.topology.{
|
import com.digitalasset.canton.participant.topology.TopologyComponentFactory
|
||||||
TopologyComponentFactory,
|
|
||||||
TopologyComponentFactoryX,
|
|
||||||
}
|
|
||||||
import com.digitalasset.canton.protocol.StaticDomainParameters
|
import com.digitalasset.canton.protocol.StaticDomainParameters
|
||||||
import com.digitalasset.canton.resource.Storage
|
import com.digitalasset.canton.resource.Storage
|
||||||
import com.digitalasset.canton.store.{IndexedDomain, IndexedStringStore, SequencedEventStore}
|
import com.digitalasset.canton.store.{IndexedDomain, IndexedStringStore, SequencedEventStore}
|
||||||
@ -296,7 +293,7 @@ class SyncDomainPersistentStateManagerX(
|
|||||||
|
|
||||||
override def topologyFactoryFor(domainId: DomainId): Option[TopologyComponentFactory] = {
|
override def topologyFactoryFor(domainId: DomainId): Option[TopologyComponentFactory] = {
|
||||||
get(domainId).map(state =>
|
get(domainId).map(state =>
|
||||||
new TopologyComponentFactoryX(
|
new TopologyComponentFactory(
|
||||||
domainId,
|
domainId,
|
||||||
crypto,
|
crypto,
|
||||||
clock,
|
clock,
|
||||||
|
@ -12,7 +12,7 @@ import com.digitalasset.canton.config.{
|
|||||||
ProcessingTimeout,
|
ProcessingTimeout,
|
||||||
TopologyConfig,
|
TopologyConfig,
|
||||||
}
|
}
|
||||||
import com.digitalasset.canton.crypto.{Crypto, DomainSyncCryptoClient}
|
import com.digitalasset.canton.crypto.Crypto
|
||||||
import com.digitalasset.canton.data.CantonTimestamp
|
import com.digitalasset.canton.data.CantonTimestamp
|
||||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||||
import com.digitalasset.canton.participant.event.RecordOrderPublisher
|
import com.digitalasset.canton.participant.event.RecordOrderPublisher
|
||||||
@ -37,54 +37,7 @@ import com.digitalasset.canton.version.ProtocolVersion
|
|||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
trait TopologyComponentFactory {
|
class TopologyComponentFactory(
|
||||||
|
|
||||||
def createTopologyClient(
|
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
|
||||||
)(implicit executionContext: ExecutionContext): DomainTopologyClientWithInit
|
|
||||||
|
|
||||||
def createCachingTopologyClient(
|
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
|
||||||
)(implicit
|
|
||||||
executionContext: ExecutionContext,
|
|
||||||
traceContext: TraceContext,
|
|
||||||
): Future[DomainTopologyClientWithInit]
|
|
||||||
|
|
||||||
def createTopologySnapshot(
|
|
||||||
asOf: CantonTimestamp,
|
|
||||||
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
|
||||||
preferCaching: Boolean,
|
|
||||||
)(implicit executionContext: ExecutionContext): TopologySnapshot
|
|
||||||
|
|
||||||
def createHeadTopologySnapshot()(implicit
|
|
||||||
executionContext: ExecutionContext
|
|
||||||
): TopologySnapshot =
|
|
||||||
createTopologySnapshot(
|
|
||||||
CantonTimestamp.MaxValue,
|
|
||||||
StoreBasedDomainTopologyClient.NoPackageDependencies,
|
|
||||||
preferCaching = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
def createTopologyProcessorFactory(
|
|
||||||
partyNotifier: LedgerServerPartyNotifier,
|
|
||||||
missingKeysAlerter: MissingKeysAlerter,
|
|
||||||
topologyClient: DomainTopologyClientWithInit,
|
|
||||||
// this is the client above, wrapped with some crypto methods, but only the base client is accessible, so we
|
|
||||||
// need to pass both.
|
|
||||||
// TODO(#15208) remove me with 3.0
|
|
||||||
syncCrypto: DomainSyncCryptoClient,
|
|
||||||
trafficStateController: TrafficStateController,
|
|
||||||
recordOrderPublisher: RecordOrderPublisher,
|
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
useNewTrafficControl: Boolean,
|
|
||||||
): TopologyTransactionProcessorCommon.Factory
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(#15161) collapse with base trait
|
|
||||||
class TopologyComponentFactoryX(
|
|
||||||
domainId: DomainId,
|
domainId: DomainId,
|
||||||
crypto: Crypto,
|
crypto: Crypto,
|
||||||
clock: Clock,
|
clock: Clock,
|
||||||
@ -95,16 +48,14 @@ class TopologyComponentFactoryX(
|
|||||||
topologyXConfig: TopologyConfig,
|
topologyXConfig: TopologyConfig,
|
||||||
topologyStore: TopologyStoreX[DomainStore],
|
topologyStore: TopologyStoreX[DomainStore],
|
||||||
loggerFactory: NamedLoggerFactory,
|
loggerFactory: NamedLoggerFactory,
|
||||||
) extends TopologyComponentFactory {
|
) {
|
||||||
|
|
||||||
override def createTopologyProcessorFactory(
|
def createTopologyProcessorFactory(
|
||||||
partyNotifier: LedgerServerPartyNotifier,
|
partyNotifier: LedgerServerPartyNotifier,
|
||||||
missingKeysAlerter: MissingKeysAlerter,
|
missingKeysAlerter: MissingKeysAlerter,
|
||||||
topologyClient: DomainTopologyClientWithInit,
|
topologyClient: DomainTopologyClientWithInit,
|
||||||
syncCrypto: DomainSyncCryptoClient,
|
|
||||||
trafficStateController: TrafficStateController,
|
trafficStateController: TrafficStateController,
|
||||||
recordOrderPublisher: RecordOrderPublisher,
|
recordOrderPublisher: RecordOrderPublisher,
|
||||||
protocolVersion: ProtocolVersion,
|
|
||||||
useNewTrafficControl: Boolean,
|
useNewTrafficControl: Boolean,
|
||||||
): TopologyTransactionProcessorCommon.Factory = new TopologyTransactionProcessorCommon.Factory {
|
): TopologyTransactionProcessorCommon.Factory = new TopologyTransactionProcessorCommon.Factory {
|
||||||
override def create(
|
override def create(
|
||||||
@ -139,7 +90,7 @@ class TopologyComponentFactoryX(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def createTopologyClient(
|
def createTopologyClient(
|
||||||
protocolVersion: ProtocolVersion,
|
protocolVersion: ProtocolVersion,
|
||||||
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
||||||
)(implicit executionContext: ExecutionContext): DomainTopologyClientWithInit =
|
)(implicit executionContext: ExecutionContext): DomainTopologyClientWithInit =
|
||||||
@ -154,7 +105,7 @@ class TopologyComponentFactoryX(
|
|||||||
loggerFactory,
|
loggerFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
override def createCachingTopologyClient(
|
def createCachingTopologyClient(
|
||||||
protocolVersion: ProtocolVersion,
|
protocolVersion: ProtocolVersion,
|
||||||
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
||||||
)(implicit
|
)(implicit
|
||||||
@ -173,7 +124,7 @@ class TopologyComponentFactoryX(
|
|||||||
loggerFactory,
|
loggerFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
override def createTopologySnapshot(
|
def createTopologySnapshot(
|
||||||
asOf: CantonTimestamp,
|
asOf: CantonTimestamp,
|
||||||
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
packageDependencies: PackageId => EitherT[Future, PackageId, Set[PackageId]],
|
||||||
preferCaching: Boolean,
|
preferCaching: Boolean,
|
||||||
@ -190,4 +141,12 @@ class TopologyComponentFactoryX(
|
|||||||
snapshot
|
snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def createHeadTopologySnapshot()(implicit
|
||||||
|
executionContext: ExecutionContext
|
||||||
|
): TopologySnapshot =
|
||||||
|
createTopologySnapshot(
|
||||||
|
CantonTimestamp.MaxValue,
|
||||||
|
StoreBasedDomainTopologyClient.NoPackageDependencies,
|
||||||
|
preferCaching = false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,30 @@ class PackageServiceTest extends AsyncWordSpec with BaseTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"validate DAR and packages from bytes" in withEnv { env =>
|
||||||
|
import env.*
|
||||||
|
|
||||||
|
val expectedPackageIdsAndState = examplePackages
|
||||||
|
.map(DamlPackageStore.readPackageId)
|
||||||
|
.map(PackageDescription(_, cantonExamplesDescription))
|
||||||
|
|
||||||
|
for {
|
||||||
|
hash <- sut
|
||||||
|
.validateByteString(
|
||||||
|
ByteString.copyFrom(bytes),
|
||||||
|
"some/path/CantonExamples.dar",
|
||||||
|
)
|
||||||
|
.value
|
||||||
|
.map(_.valueOrFail("couldn't validate a dar file"))
|
||||||
|
.failOnShutdown
|
||||||
|
packages <- packageStore.listPackages()
|
||||||
|
dar <- packageStore.getDar(hash)
|
||||||
|
} yield {
|
||||||
|
expectedPackageIdsAndState.foreach(packages should not contain _)
|
||||||
|
dar shouldBe None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"fetching dependencies" in withEnv { env =>
|
"fetching dependencies" in withEnv { env =>
|
||||||
import env.*
|
import env.*
|
||||||
|
|
||||||
@ -175,6 +199,29 @@ class PackageServiceTest extends AsyncWordSpec with BaseTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"validateDar validates the package" in withEnv { env =>
|
||||||
|
import env.*
|
||||||
|
|
||||||
|
val badDarPath = PackageServiceTest.badDarPath
|
||||||
|
val payload = BinaryFileUtil
|
||||||
|
.readByteStringFromFile(badDarPath)
|
||||||
|
.valueOrFail(s"could not load bad dar file at $badDarPath")
|
||||||
|
for {
|
||||||
|
error <- leftOrFail(
|
||||||
|
sut.validateByteString(
|
||||||
|
payload,
|
||||||
|
badDarPath,
|
||||||
|
)
|
||||||
|
)("append illformed.dar").failOnShutdown
|
||||||
|
} yield {
|
||||||
|
error match {
|
||||||
|
case validation: PackageServiceErrors.Validation.ValidationError.Error =>
|
||||||
|
validation.validationError shouldBe a[com.daml.lf.validation.ETypeMismatch]
|
||||||
|
case _ => fail(s"$error is not a validation error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"appendDar validates the package" in withEnv { env =>
|
"appendDar validates the package" in withEnv { env =>
|
||||||
import env.*
|
import env.*
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
20240419.13137.v501a60ec
|
20240419.13141.v8e75f5c1
|
||||||
|
@ -707,7 +707,7 @@ abstract class UpgradesSpec(val suffix: String)
|
|||||||
case _ => {}
|
case _ => {}
|
||||||
}
|
}
|
||||||
cantonLogSrc should include regex (
|
cantonLogSrc should include regex (
|
||||||
s"KNOWN_DAR_VERSION\\(.+,.+\\): A DAR with the same version number has previously been uploaded. err-context:\\{existingPackage=$testPackageV2Id, location=.+, packageVersion=$packageVersion, uploadedPackage=$testPackageV1Id\\}"
|
s"KNOWN_DAR_VERSION\\(.+,.+\\): A DAR with the same version number has previously been uploaded. err-context:\\{existingPackage=$testPackageV2Id, location=.+, packageVersion=$packageVersion, uploadedPackageId=$testPackageV1Id\\}"
|
||||||
)
|
)
|
||||||
uploadV2Result match {
|
uploadV2Result match {
|
||||||
case None =>
|
case None =>
|
||||||
|
Loading…
Reference in New Issue
Block a user