Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- The synchronizer id from a RegisteredSynchronizer, so the SV app can serve a registration by
-- synchronizer id from its DSO store without a JSON extraction on every candidate row.
alter table dso_acs_store
add column registered_synchronizer_id text;
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ object SqlIndexInitializationTrigger {
where registered_synchronizer_id is not null
""",
),
IndexAction
.Create(
indexName = "dso_acs_store_sid_mid_pn_tid_rsid",
createAction = sqlu"""
create index concurrently if not exists dso_acs_store_sid_mid_pn_tid_rsid
on dso_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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class SqlIndexInitializationTriggerStoreTest
"updt_hist_tran_hi_eth",
"dso_acs_store_sid_mid_pn_tid_rbio",
"scan_acs_store_sid_mid_pn_tid_rsid",
"dso_acs_store_sid_mid_pn_tid_rsid",
"scan_txlog_store_sid_effat_en_vot",
)

Expand Down
48 changes: 45 additions & 3 deletions apps/sv/src/main/openapi/sv-internal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ paths:
operationId: "lookupSynchronizerRegistration"
description: |
Look up the registration of a dedicated synchronizer, so the SV UI can tell a
proposer that a synchronizer id is already registered. Forwarded to Scan, which
holds the registry; the SV app does not ingest it.
proposer that a synchronizer id is already registered, and resolve the registration
an offboard or set-parameters proposal targets. Served from the SV app's DSO store.
parameters:
- name: "synchronizer_id"
in: "path"
Expand All @@ -255,9 +255,32 @@ paths:
content:
application/json:
schema:
"$ref": "../../../../scan/src/main/openapi/scan.yaml#/components/schemas/LookupSynchronizerRegistrationResponse"
$ref: "#/components/schemas/LookupSynchronizerRegistrationResponse"
"404":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404"
/v0/admin/sv/synchronizers/registrations/by-contract-id/{contract_id}:
get:
tags: [ sv ]
x-jvm-package: sv_operator
operationId: "lookupSynchronizerRegistrationByContractId"
description: |
Look up an active synchronizer registration by contract id, so the SV UI can show
what an offboard or set-parameters vote targets and flag a vote whose registration
is no longer active. Empty when the registration was archived. Served from the SV
app's DSO store.
parameters:
- name: "contract_id"
in: "path"
required: true
schema:
type: string
responses:
"200":
description: ok
content:
application/json:
schema:
$ref: "#/components/schemas/LookupSynchronizerRegistrationByContractIdResponse"
/v0/admin/sv/voteresults/count:
post:
tags: [ sv ]
Expand Down Expand Up @@ -1234,6 +1257,25 @@ components:
featured_app_right:
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/schemas/Contract"

LookupSynchronizerRegistrationResponse:
description: |
An active `Splice.DecentralizedSynchronizer:RegisteredSynchronizer` contract for a
synchronizer id.
type: object
required: ["registration"]
properties:
registration:
$ref: "../../../../common/src/main/openapi/common-internal.yaml#/components/schemas/ContractWithState"

LookupSynchronizerRegistrationByContractIdResponse:
description: |
An active `Splice.DecentralizedSynchronizer:RegisteredSynchronizer` contract looked up by
contract ID, if it exists.
type: object
properties:
registration:
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/schemas/Contract"

ListFeaturedAppRightsByProviderResponse:
description: |
The `Splice.Amulet.FeaturedAppRight` contract for a specific provider, if it exists.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ class HttpSvOperatorHandler(
}

/** Intended use: the SV app UI, to warn a proposer that a synchronizer id is already
* registered. Forwarded to Scan, which holds the registry; the SV app does not ingest it.
* registered, and to resolve the registration an offboard or set-parameters proposal
* targets. The DSO party signs every registration, so the SV app's own DSO store holds them.
*/
override def lookupSynchronizerRegistration(
respond: r0.LookupSynchronizerRegistrationResponse.type
Expand All @@ -260,8 +261,7 @@ class HttpSvOperatorHandler(
implicit val ActAsKnownUserRequest(traceContext) = extracted
withSpan(s"$workflowId.lookupSynchronizerRegistration") { _ => _ =>
for {
scanConnection <- scanConnectionF
registrationOpt <- scanConnection.lookupSynchronizerRegistration(synchronizerId)
registrationOpt <- dsoStore.lookupSynchronizerRegistration(synchronizerId)
} yield registrationOpt match {
case None =>
r0.LookupSynchronizerRegistrationResponse.NotFound(
Expand All @@ -277,6 +277,29 @@ class HttpSvOperatorHandler(
}
}

/** Intended use: the SV app UI, to show what an offboard or set-parameters vote targets and
* to flag a vote whose pinned registration is no longer active. The DSO party signs every
* registration, so the SV app's own DSO store holds them.
*/
override def lookupSynchronizerRegistrationByContractId(
respond: r0.LookupSynchronizerRegistrationByContractIdResponse.type
)(contractId: String)(
extracted: ActAsKnownUserRequest
): Future[r0.LookupSynchronizerRegistrationByContractIdResponse] = {
implicit val ActAsKnownUserRequest(traceContext) = extracted
withSpan(s"$workflowId.lookupSynchronizerRegistrationByContractId") { _ => _ =>
for {
registration <- dsoStore.multiDomainAcsStore.lookupContractById(
spliceCodegen.decentralizedsynchronizer.RegisteredSynchronizer.COMPANION
)(new spliceCodegen.decentralizedsynchronizer.RegisteredSynchronizer.ContractId(contractId))
} yield respond.OK(
definitions.LookupSynchronizerRegistrationByContractIdResponse(
registration.map(_.contract.toHttp)
)
)
}
}

override def listValidatorLicenses(
respond: r0.ListValidatorLicensesResponse.type
)(after: Option[Long], limit: Option[Int])(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,14 @@ trait SvDsoStore
]
]

/** The active registration for a dedicated synchronizer id, if any. */
def lookupSynchronizerRegistration(synchronizerId: String)(implicit
tc: TraceContext
): Future[Option[ContractWithState[
splice.decentralizedsynchronizer.RegisteredSynchronizer.ContractId,
splice.decentralizedsynchronizer.RegisteredSynchronizer,
]]]

def lookupFeaturedAppRight(
providerPartyId: PartyId
)(implicit
Expand Down Expand Up @@ -1507,6 +1515,19 @@ object SvDsoStore {
totalTrafficPurchased = Some(contract.payload.totalPurchased),
)
},
// The SV UI looks a registration up here by contract id, to show what an offboard or
// set-parameters vote targets, and by synchronizer id, to reject a duplicate registration.
mkFilter(splice.decentralizedsynchronizer.RegisteredSynchronizer.COMPANION)(co =>
co.payload.dso == dso
)(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.
DsoAcsStoreRowData(
contract,
registeredSynchronizerId = Some(contract.payload.synchronizerId),
)
),
mkFilter(splice.ans.AnsRules.COMPANION)(co => co.payload.dso == dso)(DsoAcsStoreRowData(_)),
mkFilter(splice.ans.AnsEntry.COMPANION)(co => co.payload.dso == dso) { contract =>
DsoAcsStoreRowData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import com.daml.nonempty.NonEmpty
import org.lfdecentralizedtrust.splice.automation.MultiDomainExpiredContractTrigger.ListExpiredContracts
import org.lfdecentralizedtrust.splice.codegen.java.splice
import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.*
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.round.{
ClosedMiningRound,
SummarizingMiningRound,
Expand Down Expand Up @@ -1956,6 +1959,31 @@ class DbSvDsoStore(
} yield row.map(contractWithStateFromRow(AnsEntryContext.COMPANION)(_))
}

override def lookupSynchronizerRegistration(synchronizerId: String)(implicit
tc: TraceContext
): Future[Option[ContractWithState[RegisteredSynchronizer.ContractId, RegisteredSynchronizer]]] =
waitUntilAcsIngested {
for {
row <- storage
.querySingle(
selectFromAcsTableWithState(
DsoTables.acsTableName,
acsStoreId,
domainMigrationId,
RegisteredSynchronizer.COMPANION,
additionalWhere = sql"""and registered_synchronizer_id = ${lengthLimited(
synchronizerId
)}""",
// Uniqueness is not enforced on-ledger, so pick a total order: a duplicate
// registration then always resolves to the same row.
orderLimit = sql"""order by contract_id limit 1""",
).headOption,
"lookupSynchronizerRegistration",
)
.value
} yield row.map(contractWithStateFromRow(RegisteredSynchronizer.COMPANION)(_))
}

override def listClosedRounds(
roundNumbers: Set[Long],
synchronizerId: SynchronizerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ object DsoTables extends AcsTables with NamedLogging {
walletParty: Option[PartyId] = None,
conversionRateFeedPublisher: Option[PartyId] = None,
rewardBeneficiaryIsObserver: Option[Boolean] = None,
registeredSynchronizerId: Option[String] = None,
) extends AcsRowData.AcsRowDataFromContract {
override def indexColumns: Seq[(String, IndexColumnValue[?])] = Seq(
DsoAcsStoreRowData.IndexColumns.amulet_round_of_expiry -> amuletRoundOfExpiry,
Expand Down Expand Up @@ -91,6 +92,8 @@ object DsoTables extends AcsTables with NamedLogging {
DsoAcsStoreRowData.IndexColumns.wallet_party -> walletParty,
DsoAcsStoreRowData.IndexColumns.conversion_rate_feed_publisher -> conversionRateFeedPublisher,
DsoAcsStoreRowData.IndexColumns.reward_beneficiary_is_observer -> rewardBeneficiaryIsObserver,
DsoAcsStoreRowData.IndexColumns.registered_synchronizer_id -> registeredSynchronizerId
.map(lengthLimited),
)
}
object DsoAcsStoreRowData {
Expand Down Expand Up @@ -133,6 +136,7 @@ object DsoTables extends AcsTables with NamedLogging {
val wallet_party = "wallet_party"
val conversion_rate_feed_publisher = "conversion_rate_feed_publisher"
val reward_beneficiary_is_observer = "reward_beneficiary_is_observer"
val registered_synchronizer_id = "registered_synchronizer_id"
val All = Seq(
amulet_round_of_expiry,
reward_round,
Expand Down Expand Up @@ -166,6 +170,7 @@ object DsoTables extends AcsTables with NamedLogging {
wallet_party,
conversion_rate_feed_publisher,
reward_beneficiary_is_observer,
registered_synchronizer_id,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{
}
import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.*
import org.lfdecentralizedtrust.splice.codegen.java.splice.cometbft.CometBftConfigLimits
import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.MemberTraffic
import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.{
GovernanceParameters,
MemberTraffic,
RegisteredSynchronizer,
}
import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.decentralizedsynchronizer.{
DsoDecentralizedSynchronizerConfig,
SynchronizerNodeConfigLimits,
Expand Down Expand Up @@ -51,6 +55,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.payment.{Payme
import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.subscriptions.*
import org.lfdecentralizedtrust.splice.config.IngestionConfig
import org.lfdecentralizedtrust.splice.environment.{DarResources, RetryProvider}
import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.ContractState.Assigned
import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.QueryResult
import org.lfdecentralizedtrust.splice.store.{
HardLimit,
Expand All @@ -67,6 +72,7 @@ import org.lfdecentralizedtrust.splice.sv.util.SvUtil
import org.lfdecentralizedtrust.splice.util.{
AssignedContract,
Contract,
ContractWithState,
ResourceTemplateDecoder,
TemplateJsonDecoder,
}
Expand Down Expand Up @@ -209,6 +215,75 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext {
_.lookupFeaturedAppRightWithOffset(userParty(1))
)

"RegisteredSynchronizer" should {
def lookup(store: SvDsoStore, cid: RegisteredSynchronizer.ContractId) =
store.multiDomainAcsStore.lookupContractById(RegisteredSynchronizer.COMPANION)(cid)

"be found by contract 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)
result <- lookup(store, wanted.contractId)
} yield result.map(_.contract) should be(Some(wanted))
}

// An offboard or set-parameters vote archives the registration it pins, which leaves any
// other vote pinning it stale.
"not be found once archived" in {
val registration = registeredSynchronizer(userParty(1), "dedicated::1220aa")
for {
store <- mkStore()
_ <- dummyDomain.create(registration)(store.multiDomainAcsStore)
_ <- dummyDomain.archive(registration)(store.multiDomainAcsStore)
result <- lookup(store, registration.contractId)
} yield result should be(None)
}

// The SV UI rejects a proposal to register a synchronizer id that already has one.
"be found by 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)
found <- store.lookupSynchronizerRegistration("dedicated::1220aa")
missing <- store.lookupSynchronizerRegistration("dedicated::1220zz")
} yield {
found should be(Some(ContractWithState(wanted, Assigned(dummyDomain))))
missing should be(None)
}
}

"not be found by synchronizer id once archived" in {
val registration = registeredSynchronizer(userParty(1), "dedicated::1220aa")
for {
store <- mkStore()
_ <- dummyDomain.create(registration)(store.multiDomainAcsStore)
_ <- dummyDomain.archive(registration)(store.multiDomainAcsStore)
result <- store.lookupSynchronizerRegistration("dedicated::1220aa")
} yield result should be(None)
}

// Governance can create two registrations for one synchronizer id: the template has no
// key and DsoRules_RegisterSynchronizer creates unconditionally.
"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)
result <- store.lookupSynchronizerRegistration("dedicated::1220aa")
} yield result should be(Some(ContractWithState(lower, Assigned(dummyDomain))))
}
}

"getOpenMiningRoundTriple" should {

"return the oldest, middle, newest mining rounds" in {
Expand Down Expand Up @@ -2286,6 +2361,18 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext {
)
}

private def registeredSynchronizer(operator: PartyId, synchronizerId: String) =
contract(
RegisteredSynchronizer.TEMPLATE_ID_WITH_PACKAGE_ID,
new RegisteredSynchronizer.ContractId(nextCid()),
new RegisteredSynchronizer(
dsoParty.toProtoPrimitive,
synchronizerId,
operator.toProtoPrimitive,
new GovernanceParameters(java.math.BigDecimal.ONE.setScale(10)),
),
)

private def memberTraffic(
member: Member,
synchronizerId: SynchronizerId,
Expand Down
Loading