diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala index 18eaf51dbf..c825a2c28d 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala @@ -24,6 +24,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.round.{ OpenMiningRound, } import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.AnsRules +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer import org.lfdecentralizedtrust.splice.config.NetworkAppClientConfig import org.lfdecentralizedtrust.splice.environment.SpliceConsoleEnvironment import org.lfdecentralizedtrust.splice.http.v0.definitions @@ -196,6 +197,14 @@ abstract class ScanAppReference( httpCommand(HttpScanAppClient.LookupTransferPreapprovalByParty(party)) } + @Help.Summary("Lookup the RegisteredSynchronizer for a dedicated synchronizer id") + def lookupSynchronizerRegistration( + synchronizerId: String + ): Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]] = + consoleEnvironment.run { + httpCommand(HttpScanAppClient.LookupSynchronizerRegistration(synchronizerId)) + } + @Help.Summary("Lookup a TransferCommandCounter by the receiver party") def lookupTransferCommandCounterByParty( party: PartyId diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorTrafficIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorTrafficIntegrationTest.scala index 298b5a8fc2..f654871ab1 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorTrafficIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorTrafficIntegrationTest.scala @@ -3,7 +3,6 @@ package org.lfdecentralizedtrust.splice.integration.tests -import com.daml.ledger.javaapi.data.CreatedEvent import com.digitalasset.canton.SynchronizerAlias import com.digitalasset.canton.config.RequireTypes.NonNegativeLong import com.digitalasset.canton.data.CantonTimestamp @@ -19,9 +18,7 @@ import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTest, SpliceTestConsoleEnvironment, } -import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState import org.lfdecentralizedtrust.splice.util.{ - Contract, ContractWithState, DisclosedContracts, SynchronizerFeesTestUtil, @@ -84,10 +81,8 @@ class SyncOperatorTrafficIntegrationTest } } - val registration = clue("the DSO registers the synchronizer to this operator") { - // The purchase is submitted from alice's participant, which hosts neither the DSO nor - // the operator, so the registration must be disclosed with its created-event blob. - val tx = sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands + clue("the DSO registers the synchronizer to this operator") { + sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands .submitJava( actAs = Seq(dsoParty), readAs = Seq(dsoParty), @@ -100,16 +95,14 @@ class SyncOperatorTrafficIntegrationTest .asScala .toSeq, userId = sv1Backend.config.ledgerApiUser, - includeCreatedEventBlob = true, ) - val contract = tx.getEventsById.values.asScala - .collect { case ev: CreatedEvent => ev } - .flatMap(Contract.fromCreatedEvent(RegisteredSynchronizer.COMPANION)(_)) - .loneElement - ContractWithState( - contract, - ContractState.Assigned(SynchronizerId.tryFromString(tx.getSynchronizerId)), - ) + } + + // Alice's participant hosts neither the DSO nor the operator, so the registration has to + // be disclosed, and Scan is the only source of its created-event blob. + sv1ScanBackend.lookupSynchronizerRegistration("dedicated::does-not-exist") shouldBe None + val registration = eventually() { + sv1ScanBackend.lookupSynchronizerRegistration(synchronizerId.toProtoPrimitive).value } val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) diff --git a/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V075__scan_acs_store_registered_synchronizer.sql b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V075__scan_acs_store_registered_synchronizer.sql new file mode 100644 index 0000000000..e0cfe59a99 --- /dev/null +++ b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V075__scan_acs_store_registered_synchronizer.sql @@ -0,0 +1,4 @@ +-- The synchronizer id from a RegisteredSynchronizer, so Scan can serve a registration by +-- synchronizer id without a JSON extraction on every candidate row. +alter table scan_acs_store + add column registered_synchronizer_id text; diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala index 9e72fdf48c..0cf497a95e 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTrigger.scala @@ -264,6 +264,15 @@ object SqlIndexInitializationTrigger { where reward_beneficiary_is_observer = false """, ), + IndexAction + .Create( + indexName = "scan_acs_store_sid_mid_pn_tid_rsid", + createAction = sqlu""" + create index concurrently if not exists scan_acs_store_sid_mid_pn_tid_rsid + on scan_acs_store (store_id, migration_id, package_name, template_id_qualified_name, registered_synchronizer_id) + where registered_synchronizer_id is not null + """, + ), IndexAction .Create( indexName = "scan_txlog_store_sid_effat_en_vot", diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala index 0fbbcfe077..dec965b892 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/automation/SqlIndexInitializationTriggerStoreTest.scala @@ -42,6 +42,7 @@ class SqlIndexInitializationTriggerStoreTest "updt_hist_crea_hi_mi_ci_import_updates", "updt_hist_tran_hi_eth", "dso_acs_store_sid_mid_pn_tid_rbio", + "scan_acs_store_sid_mid_pn_tid_rsid", "scan_txlog_store_sid_effat_en_vot", ) diff --git a/apps/scan/src/main/openapi/scan.yaml b/apps/scan/src/main/openapi/scan.yaml index efbc94a9f4..1df9ead450 100644 --- a/apps/scan/src/main/openapi/scan.yaml +++ b/apps/scan/src/main/openapi/scan.yaml @@ -1124,6 +1124,32 @@ paths: "404": $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" + /v0/synchronizers/{synchronizer_id}/registration: + get: + tags: [internal, scan] + x-jvm-package: scan + operationId: "lookupSynchronizerRegistration" + description: | + Get the RegisteredSynchronizer authorizing Amulet-funded traffic for a dedicated + synchronizer. + parameters: + - name: "synchronizer_id" + description: | + The synchronizer ID whose registration to look up. + in: "path" + required: true + schema: + type: string + responses: + "200": + description: ok + content: + application/json: + schema: + "$ref": "#/components/schemas/LookupSynchronizerRegistrationResponse" + "404": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" + /v0/transfer-command-counter/{party}: get: tags: [internal, scan] @@ -3531,6 +3557,14 @@ components: properties: transfer_preapproval: "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/ContractWithState" + LookupSynchronizerRegistrationResponse: + description: A Daml contract of template `Splice.DecentralizedSynchronizer:RegisteredSynchronizer`. + type: object + required: ["registration"] + properties: + registration: + "$ref": "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/ContractWithState" + LookupTransferCommandCounterByPartyResponse: description: A Daml contract of template `Splice.ExternalPartyAmuletRules:TransferCommandCounter`. type: object diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala index 30802acef6..4b3613889b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala @@ -104,6 +104,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationi import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationinstructionv2 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv1 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv2 +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ DsoRules, DsoRules_CloseVoteRequestResult, @@ -483,6 +484,12 @@ class BftScanConnection( ): Future[Option[LookupTransferCommandStatusResponse]] = bftCall(_.lookupTransferCommandStatus(sender, nonce), "lookupTransferCommandStatus") + override def lookupSynchronizerRegistration(synchronizerId: String)(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]]] = + bftCall(_.lookupSynchronizerRegistration(synchronizerId), "lookupSynchronizerRegistration") + override def lookupTransferPreapprovalByParty(receiver: PartyId)(implicit ec: ExecutionContext, tc: TraceContext, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala index 1aff1a2ffc..6ad82f6c8b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala @@ -20,6 +20,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.round.{ OpenMiningRound, } import org.lfdecentralizedtrust.splice.codegen.java.splice.types.Round +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer import org.lfdecentralizedtrust.splice.config.UpgradesConfig import org.lfdecentralizedtrust.splice.environment.* import org.lfdecentralizedtrust.splice.http.HttpClient @@ -308,6 +309,11 @@ trait ScanConnection tc: TraceContext, ): Future[Option[ContractWithState[TransferPreapproval.ContractId, TransferPreapproval]]] + def lookupSynchronizerRegistration(synchronizerId: String)(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]]] + def listVoteRequestResults( filters: VoteResultsFilters, limit: Int, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala index a0af1d2526..75f3d986a7 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala @@ -87,6 +87,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationv import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationv2 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationinstructionv1 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationinstructionv2 +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequest.RecordTimeMatch import org.lfdecentralizedtrust.splice.metrics.ScanConnectionMetrics import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.BftSequencer @@ -490,6 +491,15 @@ class SingleScanConnection private[client] ( HttpScanAppClient.GetMigrationId(), ) + override def lookupSynchronizerRegistration(synchronizerId: String)(implicit + ec: ExecutionContext, + tc: TraceContext, + ): Future[Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]]] = + runHttpCmd( + config.adminApi.url, + HttpScanAppClient.LookupSynchronizerRegistration(synchronizerId), + ) + override def lookupTransferPreapprovalByParty(receiver: PartyId)(implicit ec: ExecutionContext, tc: TraceContext, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala index eb15cf4536..776d2aa4e5 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala @@ -32,6 +32,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.round.{ } import org.lfdecentralizedtrust.splice.codegen.java.splice.ans as ansCodegen import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.AnsRules +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer import org.lfdecentralizedtrust.splice.config.SpliceInstanceNamesConfig import org.lfdecentralizedtrust.splice.http.v0.{definitions, scan as http} import org.lfdecentralizedtrust.tokenstandard.{ @@ -602,6 +603,30 @@ object HttpScanAppClient { } } + case class LookupSynchronizerRegistration( + synchronizerId: String + ) extends InternalBaseCommand[http.LookupSynchronizerRegistrationResponse, Option[ + ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer] + ]] { + + override def submitRequest( + client: ScanClient, + headers: List[HttpHeader], + ) = client.lookupSynchronizerRegistration(synchronizerId, headers) + + override def handleOk()(implicit + decoder: TemplateJsonDecoder + ) = { + case http.LookupSynchronizerRegistrationResponse.OK(response) => + ContractWithState + .fromHttp(RegisteredSynchronizer.COMPANION)(response.registration) + .map(Some(_)) + .leftMap(_.toString) + case http.LookupSynchronizerRegistrationResponse.NotFound(_) => + Right(None) + } + } + case class LookupTransferCommandCounterByParty( party: PartyId ) extends InternalBaseCommand[http.LookupTransferCommandCounterByPartyResponse, Option[ diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala index 1950aead98..9a139e24f8 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala @@ -1185,6 +1185,32 @@ class HttpScanHandler( } } + override def lookupSynchronizerRegistration( + respond: ScanResource.LookupSynchronizerRegistrationResponse.type + )( + synchronizerId: String + )(extracted: TraceContext): Future[ScanResource.LookupSynchronizerRegistrationResponse] = { + implicit val tc = extracted + withSpan(s"$workflowId.lookupSynchronizerRegistration") { _ => _ => + store + .lookupSynchronizerRegistration(synchronizerId) + .map { + case Some(c) => + v0.ScanResource.LookupSynchronizerRegistrationResponse.OK( + definitions.LookupSynchronizerRegistrationResponse( + c.toHttp + ) + ) + case None => + v0.ScanResource.LookupSynchronizerRegistrationResponse.NotFound( + definitions.ErrorResponse( + s"No RegisteredSynchronizer found for synchronizer id: $synchronizerId" + ) + ) + } + } + } + override def lookupTransferCommandCounterByParty( respond: ScanResource.LookupTransferCommandCounterByPartyResponse.type )( diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala index dec98aef00..dba8190351 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala @@ -17,6 +17,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{ TransferPreapproval, } import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.{AnsEntry, AnsRules} +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.svstate.SvNodeState import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ DsoRules_CloseVoteRequestResult, @@ -181,6 +182,11 @@ class CachingScanStore( store.lookupTransferPreapprovalByParty, ).get(partyId) + override def lookupSynchronizerRegistration(synchronizerId: String)(implicit + tc: TraceContext + ): Future[Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]]] = + store.lookupSynchronizerRegistration(synchronizerId) + override def lookupTransferCommandCounterByParty(partyId: PartyId)(implicit tc: TraceContext ): Future[Option[ContractWithState[TransferCommandCounter.ContractId, TransferCommandCounter]]] = diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala index 6ba742e2be..eb49ef45a3 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala @@ -204,6 +204,21 @@ trait ScanStore ]] ] + /** The registration authorizing Amulet-funded traffic for `synchronizerId`, if any. + * + * Comes from the ACS store, so the result carries the created event blob the buyer needs to + * disclose it. If more than one is live, returns one of them under a stable total order; no + * supersession is implied. + */ + def lookupSynchronizerRegistration( + synchronizerId: String + )(implicit tc: TraceContext): Future[ + Option[ContractWithState[ + splice.decentralizedsynchronizer.RegisteredSynchronizer.ContractId, + splice.decentralizedsynchronizer.RegisteredSynchronizer, + ]] + ] + def lookupTransferCommandCounterByParty( partyId: PartyId )(implicit tc: TraceContext): Future[ @@ -408,8 +423,14 @@ object ScanStore { pkgVersionSupport .supportsDedicatedSynchronizers(Seq(key.dsoParty), now)(tc) }, - )( - ScanAcsStoreRowData(_) + )(contract => + // A String, not a SynchronizerId: DsoRules_RegisterSynchronizer only checks the id is + // non-empty, so tryFromString here would throw on a governance typo and take down the + // ingestion pipeline. The query compares it as text. + ScanAcsStoreRowData( + contract, + registeredSynchronizerId = Some(contract.payload.synchronizerId), + ) ), mkFilter(splice.validatorlicense.ValidatorLicense.COMPANION)(co => co.payload.dso == dso) { contract => diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala index 871c2633b6..b9ea443974 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala @@ -22,7 +22,10 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{ TransferPreapproval, } import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.{AnsEntry, AnsRules} -import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.MemberTraffic +import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.{ + MemberTraffic, + RegisteredSynchronizer, +} import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.svstate.SvNodeState import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ DsoRules_CloseVoteRequestResult, @@ -365,6 +368,33 @@ class DbScanStore( } yield contractWithStateFromRow(TransferPreapproval.COMPANION)(row)).value } + override def lookupSynchronizerRegistration( + synchronizerId: String + )(implicit tc: TraceContext): Future[ + Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]] + ] = waitUntilAcsIngested { + (for { + row <- storage + .querySingle( + selectFromAcsTableWithState( + ScanTables.acsTableName, + acsStoreId, + domainMigrationId, + RegisteredSynchronizer.COMPANION, + additionalWhere = sql""" + and registered_synchronizer_id = ${lengthLimited(synchronizerId)} + """, + // Uniqueness is not enforced on-ledger, so pick a total order: every Scan must + // return the same row for bftCall to agree. + orderLimit = sql""" + order by contract_id limit 1 + """, + ).headOption, + "lookupSynchronizerRegistration", + ) + } yield contractWithStateFromRow(RegisteredSynchronizer.COMPANION)(row)).value + } + override def lookupTransferCommandCounterByParty( partyId: PartyId )(implicit tc: TraceContext): Future[ diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala index de109b94bf..20f6b0fa26 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala @@ -56,6 +56,7 @@ object ScanTables extends AcsTables { transferPreapprovalReceiver: Option[PartyId] = None, transferPreapprovalValidFrom: Option[Timestamp] = None, walletParty: Option[PartyId] = None, + registeredSynchronizerId: Option[String] = None, ) extends AcsRowData.AcsRowDataFromContract { override def indexColumns: Seq[(String, IndexColumnValue[?])] = Seq( ScanAcsStoreRowData.IndexColumns.round -> round, @@ -75,6 +76,8 @@ object ScanTables extends AcsTables { ScanAcsStoreRowData.IndexColumns.transfer_preapproval_receiver -> transferPreapprovalReceiver, ScanAcsStoreRowData.IndexColumns.transfer_preapproval_valid_from -> transferPreapprovalValidFrom, ScanAcsStoreRowData.IndexColumns.wallet_party -> walletParty, + ScanAcsStoreRowData.IndexColumns.registered_synchronizer_id -> registeredSynchronizerId + .map(lengthLimited), ) } @@ -101,6 +104,7 @@ object ScanTables extends AcsTables { val transfer_preapproval_receiver = "transfer_preapproval_receiver" val transfer_preapproval_valid_from = "transfer_preapproval_valid_from" val wallet_party = "wallet_party" + val registered_synchronizer_id = "registered_synchronizer_id" val All = Seq( round, validator, @@ -119,6 +123,7 @@ object ScanTables extends AcsTables { transfer_preapproval_receiver, transfer_preapproval_valid_from, wallet_party, + registered_synchronizer_id, ) } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala index 7227c28b63..6f7e288b88 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala @@ -240,6 +240,42 @@ abstract class ScanStoreTest } } + "lookupSynchronizerRegistration" should { + "return the registration for the requested synchronizer id" in { + val wanted = registeredSynchronizer(userParty(1), "dedicated::1220aa") + val other = registeredSynchronizer(userParty(2), "dedicated::1220bb") + for { + store <- mkStore() + _ <- dummyDomain.create(wanted)(store.multiDomainAcsStore) + _ <- dummyDomain.create(other)(store.multiDomainAcsStore) + } yield { + store.lookupSynchronizerRegistration("dedicated::1220aa").futureValue should be( + Some(ContractWithState(wanted, Assigned(dummyDomain))) + ) + store.lookupSynchronizerRegistration("dedicated::1220zz").futureValue should be(None) + } + } + + // Governance can create two registrations for one synchronizer id: the template has no + // key and DsoRules_RegisterSynchronizer creates unconditionally. Every Scan must pick the + // same one, because bftCall compares responses structurally. + "pick deterministically when a synchronizer id has more than one registration" in { + // Ingest `lower` second so insertion order and contract-id order disagree. + val lower = registeredSynchronizer(userParty(1), "dedicated::1220aa") + val higher = registeredSynchronizer(userParty(2), "dedicated::1220aa") + lower.contractId.contractId should be < higher.contractId.contractId + for { + store <- mkStore() + _ <- dummyDomain.create(higher)(store.multiDomainAcsStore) + _ <- dummyDomain.create(lower)(store.multiDomainAcsStore) + } yield { + store.lookupSynchronizerRegistration("dedicated::1220aa").futureValue should be( + Some(ContractWithState(lower, Assigned(dummyDomain))) + ) + } + } + } + "lookupTransferPreapprovalByParty" should { "return the TransferPreapproval contract signed by the specified party if available" in { val wanted = transferPreapproval(userParty(1), providerParty(1), time(0), time(1)) @@ -1714,13 +1750,16 @@ trait AmuletTransferUtil { self: StoreTestBase => ) } - def registeredSynchronizer(operator: PartyId) = + def registeredSynchronizer( + operator: PartyId, + synchronizerId: String = dummyDomain.toProtoPrimitive, + ) = contract( RegisteredSynchronizer.TEMPLATE_ID_WITH_PACKAGE_ID, new RegisteredSynchronizer.ContractId(nextCid()), new RegisteredSynchronizer( dsoParty.toProtoPrimitive, - dummyDomain.toProtoPrimitive, + synchronizerId, operator.toProtoPrimitive, ), ) diff --git a/cluster/configs/shared/rate-limits/unlimited.yaml b/cluster/configs/shared/rate-limits/unlimited.yaml index 6d9cd05373..b2115038d0 100644 --- a/cluster/configs/shared/rate-limits/unlimited.yaml +++ b/cluster/configs/shared/rate-limits/unlimited.yaml @@ -45,6 +45,10 @@ rateLimits: name: domains-v1 type: unlimited + /api/scan/v0/synchronizers: + name: synchronizers + type: unlimited + /api/scan/v0/closed-rounds: name: closed-rounds type: unlimited diff --git a/cluster/deployment/scratchneta/config.resolved.yaml b/cluster/deployment/scratchneta/config.resolved.yaml index 9ddf688b30..b9ba039558 100644 --- a/cluster/deployment/scratchneta/config.resolved.yaml +++ b/cluster/deployment/scratchneta/config.resolved.yaml @@ -333,6 +333,9 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' + /api/scan/v0/synchronizers: + name: 'synchronizers' + type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' diff --git a/cluster/deployment/scratchnetb/config.resolved.yaml b/cluster/deployment/scratchnetb/config.resolved.yaml index 9ddf688b30..b9ba039558 100644 --- a/cluster/deployment/scratchnetb/config.resolved.yaml +++ b/cluster/deployment/scratchnetb/config.resolved.yaml @@ -333,6 +333,9 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' + /api/scan/v0/synchronizers: + name: 'synchronizers' + type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' diff --git a/cluster/deployment/scratchnetc/config.resolved.yaml b/cluster/deployment/scratchnetc/config.resolved.yaml index 9ddf688b30..b9ba039558 100644 --- a/cluster/deployment/scratchnetc/config.resolved.yaml +++ b/cluster/deployment/scratchnetc/config.resolved.yaml @@ -333,6 +333,9 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' + /api/scan/v0/synchronizers: + name: 'synchronizers' + type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' diff --git a/cluster/deployment/scratchnetd/config.resolved.yaml b/cluster/deployment/scratchnetd/config.resolved.yaml index 9ddf688b30..b9ba039558 100644 --- a/cluster/deployment/scratchnetd/config.resolved.yaml +++ b/cluster/deployment/scratchnetd/config.resolved.yaml @@ -333,6 +333,9 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' + /api/scan/v0/synchronizers: + name: 'synchronizers' + type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' diff --git a/cluster/deployment/scratchnete/config.resolved.yaml b/cluster/deployment/scratchnete/config.resolved.yaml index 9ddf688b30..b9ba039558 100644 --- a/cluster/deployment/scratchnete/config.resolved.yaml +++ b/cluster/deployment/scratchnete/config.resolved.yaml @@ -333,6 +333,9 @@ sv: /api/scan/v0/synchronizer-identities: name: 'synchronizer-identities' type: 'unlimited' + /api/scan/v0/synchronizers: + name: 'synchronizers' + type: 'unlimited' /api/scan/v0/transfer-command: name: 'transfer-command-status' type: 'unlimited' diff --git a/cluster/expected/sv-runbook/expected.json b/cluster/expected/sv-runbook/expected.json index 201dda3b88..5eb098fc76 100644 --- a/cluster/expected/sv-runbook/expected.json +++ b/cluster/expected/sv-runbook/expected.json @@ -997,6 +997,10 @@ "name": "synchronizer-identities", "type": "unlimited" }, + "/api/scan/v0/synchronizers": { + "name": "synchronizers", + "type": "unlimited" + }, "/api/scan/v0/transfer-command": { "name": "transfer-command-status", "type": "unlimited" diff --git a/cluster/expected/sv/expected.json b/cluster/expected/sv/expected.json index c0157a8b85..ff2f7234bc 100644 --- a/cluster/expected/sv/expected.json +++ b/cluster/expected/sv/expected.json @@ -1692,6 +1692,10 @@ "name": "synchronizer-identities", "type": "unlimited" }, + "/api/scan/v0/synchronizers": { + "name": "synchronizers", + "type": "unlimited" + }, "/api/scan/v0/transfer-command": { "name": "transfer-command-status", "type": "unlimited" @@ -2093,6 +2097,10 @@ "name": "synchronizer-identities", "type": "unlimited" }, + "/api/scan/v0/synchronizers": { + "name": "synchronizers", + "type": "unlimited" + }, "/api/scan/v0/transfer-command": { "name": "transfer-command-status", "type": "unlimited"