Skip to content
Merged
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
Expand Up @@ -83,6 +83,12 @@ abstract class SvAppReference(
httpCommand(HttpSvPublicAppClient.getSvOnboardingStatus(candidate))
}

@Help.Summary("Buy member traffic for a new validator (DevNet only) (via client API)")
def devNetBuyMemberTraffic(participantId: ParticipantId): Unit =
consoleEnvironment.run {
httpCommand(HttpSvPublicAppClient.DevNetBuyMemberTraffic(participantId.toProtoPrimitive))
}

@Help.Summary("Prepare a validator onboarding and return an onboarding secret (via client API)")
def devNetOnboardValidatorPrepare(): String =
consoleEnvironment.run {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,25 @@ class PermissionedSynchronizerIntegrationTest
aliceValidatorBackend.onboardUser("TestUser")
},
)

clue("Sponser SV Buys Member Traffic for Bob in the DevNet") {
sv1Backend.devNetBuyMemberTraffic(bobValidatorBackend.participantClient.id)
}

clue("Verify Bob is granted ParticipantSynchronizerPermission") {
eventually() {
sv1ScanBackend.getParticipantSynchronizerPermission(
decentralizedSynchronizerId.toProtoPrimitive,
bobValidatorBackend.participantClient.id.toProtoPrimitive,
) shouldBe Some(
SynchronizerPermissionState(None)
)
}
}

clue("Bob Validator starts and onboards correctly") {
bobValidatorBackend.startSync()
bobValidatorBackend.onboardUser("TestUserBob")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ class SvDsoStoreIngestionPerformanceTest(
participantId = mkParticipantId("IngestionPerformanceIngestionTest"),
IngestionConfig(),
defaultLimit = HardLimit.tryCreate(Limit.DefaultMaxPageSize),
config = None,
)(ec, templateJsonDecoder, closeContext).multiDomainAcsStore
}

Expand Down
33 changes: 33 additions & 0 deletions apps/sv/src/main/openapi/sv-internal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,31 @@ paths:
"$ref": "#/components/schemas/OnboardSvSequencerResponse"
"400":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400"
/v0/devnet/onboard/validator/purchase-traffic:
post:
tags: [sv]
x-jvm-package: sv_public
description: "faucet for validator candidates to buy member traffic"
operationId: "devNetBuyMemberTraffic"
requestBody:
required: true
content:
application/json:
schema:
"$ref": "#/components/schemas/DevNetBuyMemberTrafficRequest"
responses:
"200":
description: ok
content:
text/plain:
schema:
type: string
"400":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400"
"500":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500"
"501":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/501"
/v0/devnet/onboard/validator/prepare:
post:
tags: [sv]
Expand Down Expand Up @@ -721,6 +746,14 @@ components:
Human-readable alias of the validator party decoded from the stored secret.
type: string

DevNetBuyMemberTrafficRequest:
type: object
required:
- participant_id
properties:
participant_id:
type: string

PrepareValidatorOnboardingRequest:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ class SvApp(
metrics,
) {

override def packagesForJsonDecoding: Seq[DarResource] =
super.packagesForJsonDecoding ++ DarResources.dsoGovernance.all ++ DarResources.validatorLifecycle.all ++ DarResources.amuletNameService.all
override def packagesForJsonDecoding: Seq[DarResource] = {
val base =
super.packagesForJsonDecoding ++ DarResources.dsoGovernance.all ++ DarResources.validatorLifecycle.all ++ DarResources.amuletNameService.all
if (config.permissionedSynchronizer) base ++ DarResources.wallet.all else base
}

override def preInitializeBeforeLedgerConnection()(implicit tc: TraceContext): Future[Unit] = {
val participantAdminConnection = new ParticipantAdminConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,28 @@ object HttpSvPublicAppClient {
}
}

case class DevNetBuyMemberTraffic(participantId: String)
extends BaseCommandPublic[http.DevNetBuyMemberTrafficResponse, Unit] {

override def submitRequest(
client: Client,
headers: List[HttpHeader],
): EitherT[Future, Either[
Throwable,
HttpResponse,
], http.DevNetBuyMemberTrafficResponse] =
client.devNetBuyMemberTraffic(
body = definitions.DevNetBuyMemberTrafficRequest(participantId),
headers = headers,
)

override def handleOk()(implicit
decoder: TemplateJsonDecoder
) = { case http.DevNetBuyMemberTrafficResponse.OK(_) =>
Right(())
}
}

case class DevNetOnboardValidatorPrepare()
extends BaseCommandPublic[http.DevNetOnboardValidatorPrepareResponse, String] {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,40 @@ class HttpSvPublicHandler(
}
}

/** Intended use: Used by validator candidates to buy member traffic using SV's DevNet faucet
*
* Protection: Rate limiting, endpoint only used for DevNet
*/
override def devNetBuyMemberTraffic(
respond: r0.DevNetBuyMemberTrafficResponse.type
)(
body: definitions.DevNetBuyMemberTrafficRequest
)(extracted: TraceContext): Future[r0.DevNetBuyMemberTrafficResponse] = {
implicit val traceContext: TraceContext = extracted
withSpan(s"$workflowId.devNetBuyMemberTraffic") { _ => _ =>
if (isDevNet && config.permissionedSynchronizer) {
for {
participantId <- ParticipantId.fromProtoPrimitive(
body.participantId,
"participant_id",
) match {
case Right(id) => Future.successful(id)
case Left(err) =>
Future.failed(HttpErrorHandler.badRequest(s"Invalid participant ID: $err"))
}
_ <- devNetTapAndBuyMemberTraffic(participantId)

} yield r0.DevNetBuyMemberTrafficResponseOK("Success")
} else {
Future.failed(
HttpErrorHandler.notImplemented(
"Traffic purchasing self-service is only available in DevNet when permissioned synchronizer is enabled."
)
)
}
}
}

/** Intended use: Used by other validators to get a free onboarding secret
*
* Protection: Rate limiting, endpoint only used for DevNet
Expand Down Expand Up @@ -850,6 +884,55 @@ class HttpSvPublicHandler(
.yieldUnit()
} yield ()

private def devNetTapAndBuyMemberTraffic(
participantId: ParticipantId
)(implicit tc: TraceContext): Future[Unit] = {
for {
dsoRules <- dsoStore.getDsoRules()

svWalletInstall <- retryProvider.retryForClientCalls(
"wait_for_wallet_install",
"Wait for SV WalletAppInstall contract to be ingested",
for {
svWalletInstallOpt <- svStoreWithIngestion.store.lookupWalletAppInstallByEndUser(svParty)
install <- svWalletInstallOpt match {
case Some(install) => Future.successful(install)
case None =>
Future.failed(
HttpErrorHandler.internalServerError(
"SV WalletAppInstall contract not found."
)
)
}
} yield install,
logger,
)

cmd = svWalletInstall.contractId.exerciseWalletAppInstall_CreateBuyTrafficRequest(
participantId.toProtoPrimitive,
dsoRules.payload.config.decentralizedSynchronizer.activeSynchronizerId,
dsoStore.domainMigrationId.toInt,
config.devNetPublicSetupTrafficAmount,
clock.now.plus(java.time.Duration.ofMinutes(5)).toInstant,
s"devnet-onboard-${participantId.toProtoPrimitive}-${clock.now.toInstant.toEpochMilli}",
)

_ = logger.info(s"Creating BuyTrafficRequest by $svUserName for $participantId")

_ <- dsoStoreWithIngestion
.connection(SpliceLedgerConnectionPriority.Medium)
.submit(
actAs = Seq(svParty),
readAs = Seq(dsoParty),
update = cmd,
)
.withSynchronizerId(dsoRules.domain)
.noDedup
.yieldUnit()

} yield ()
}

private def startSvOnboarding(
candidateName: String,
candidateParty: PartyId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ case class SvAppBackendConfig(
// where intentional overlap is required.
instanceLockEnabled: Boolean = true,
minMemberTrafficToOnboardValidator: Long = 100000L,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated tracking issue to remind myself to move these to daml

devNetPublicSetupTrafficAmount: Long = 10000000L,
) extends SpliceBackendConfig {

def allIgnoredAmuletVersions: Set[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ trait NodeInitializerUtil extends NamedLogging with Spanning with SynchronizerNo
config.automation.ingestion,
config.parameters.defaultLimit,
acsStoreDescriptorUserVersion,
Some(config),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ trait SvDsoStore
with ActiveVotesStore {
protected val outerLoggerFactory: NamedLoggerFactory
protected def templateJsonDecoder: TemplateJsonDecoder
def config: Option[org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig]
override protected lazy val loggerFactory: NamedLoggerFactory =
outerLoggerFactory.append("store", "dsoParty")

Expand All @@ -80,7 +79,6 @@ trait SvDsoStore
SvDsoStore.contractFilter(
key.dsoParty,
domainMigrationId,
config.map(_.permissionedSynchronizer).getOrElse(false),
)

def key: SvStore.Key
Expand Down Expand Up @@ -1212,7 +1210,6 @@ object SvDsoStore {
ingestionConfig: IngestionConfig,
defaultLimit: Limit,
acsStoreDescriptorUserVersion: Option[Long] = None,
svBackendconfig: Option[org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig],
)(implicit
ec: ExecutionContext,
templateJsonDecoder: TemplateJsonDecoder,
Expand All @@ -1228,15 +1225,13 @@ object SvDsoStore {
ingestionConfig,
acsStoreDescriptorUserVersion,
defaultLimit = defaultLimit,
config = svBackendconfig,
)
}

/** Contract filter of an sv acs store for a specific acs party. */
def contractFilter(
dsoParty: PartyId,
domainMigrationId: Long,
enablePermissionedSynchronizer: Boolean,
): MultiDomainAcsStore.ContractFilter[
DsoAcsStoreRowData,
AcsInterfaceViewRowData.NoInterfacesIngested,
Expand Down Expand Up @@ -1672,22 +1667,23 @@ object SvDsoStore {
contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)),
)
},
mkFilter(vl.ValidatorLicenseRequest.COMPANION)(
req => req.payload.dso == dso,
versionGuard = { case (pkgVersionSupport, now) =>
(tc) => pkgVersionSupport.supportsPermissionedSynchronizer(Seq(dsoParty), now)(tc)
},
) { contract =>
DsoAcsStoreRowData(
contract,
contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)),
validator = Some(PartyId.tryFromProtoPrimitive(contract.payload.validator)),
)
},
)

val finalFilters = if (enablePermissionedSynchronizer) {
dsoFilters + mkFilter(vl.ValidatorLicenseRequest.COMPANION)(req => req.payload.dso == dso) {
contract =>
DsoAcsStoreRowData(
contract,
contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)),
validator = Some(PartyId.tryFromProtoPrimitive(contract.payload.validator)),
)
}
} else dsoFilters

MultiDomainAcsStore.SimpleContractFilter(
dsoParty,
finalFilters,
dsoFilters,
)
}

Expand Down
Loading
Loading