-
Notifications
You must be signed in to change notification settings - Fork 0
feat: grant purchased traffic on the dedicated sequencer #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/sync-operator-e2e-test
Are you sure you want to change the base?
Changes from all commits
bca0d2f
6021ca7
f34d181
89d0017
84f9385
149d6fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package org.lfdecentralizedtrust.splice.integration.tests | ||
|
|
||
| import com.digitalasset.canton.SynchronizerAlias | ||
| import com.digitalasset.canton.data.CantonTimestamp | ||
| import com.digitalasset.canton.topology.{Member, PartyId, SynchronizerId} | ||
| import org.lfdecentralizedtrust.splice.codegen.java.splice | ||
| import org.lfdecentralizedtrust.splice.codegen.java.splice.decentralizedsynchronizer.RegisteredSynchronizer | ||
| import org.lfdecentralizedtrust.splice.codegen.java.splice.round.IssuingMiningRound | ||
| import org.lfdecentralizedtrust.splice.codegen.java.splice.types.Round | ||
| import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition | ||
| import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ | ||
| IntegrationTest, | ||
| SpliceTestConsoleEnvironment, | ||
| } | ||
| import org.lfdecentralizedtrust.splice.util.{ | ||
| DisclosedContracts, | ||
| SynchronizerFeesTestUtil, | ||
| WalletTestUtil, | ||
| } | ||
|
|
||
| import scala.jdk.CollectionConverters.* | ||
| import scala.jdk.OptionConverters.* | ||
|
|
||
| /** Buys traffic for a registered synchronizer via `AmuletRules_BuyMemberTraffic` with the | ||
| * registration disclosed, and checks the operator grants it on that synchronizer's sequencer. | ||
| */ | ||
| class SyncOperatorTrafficIntegrationTest | ||
| extends IntegrationTest | ||
| with SynchronizerFeesTestUtil | ||
| with WalletTestUtil { | ||
|
|
||
| private val firstPurchase = 1_000_000L | ||
| private val secondPurchase = 2_000_000L | ||
|
|
||
| override def environmentDefinition: SpliceEnvironmentDefinition = | ||
| EnvironmentDefinition | ||
| .fromResources( | ||
| Seq("simple-topology-1sv.conf", "sync-operator-topology.conf"), | ||
| this.getClass.getSimpleName, | ||
| ) | ||
| .withStandardSetup | ||
|
|
||
| "sync operator" should { | ||
|
|
||
| "grant traffic purchased for its synchronizer on its own sequencer" in { implicit env => | ||
| val operatorParty = syncOperatorBackend.appState.store.key.operatorParty | ||
| val dsoParty = sv1Backend.getDsoInfo().dsoParty | ||
| val dsoRules = sv1Backend.getDsoInfo().dsoRules | ||
| // The operator is pointed at the splitwell sequencer, so that is the synchronizer whose | ||
| // traffic it grants. Alice's participant is a member of it. | ||
| val synchronizerId = aliceValidatorBackend.participantClientWithAdminToken.synchronizers | ||
| .id_of(SynchronizerAlias.tryCreate("splitwell")) | ||
| .logical | ||
| val member = aliceValidatorBackend.participantClient.id | ||
|
|
||
| val registration = clue("the DSO registers the synchronizer to this operator") { | ||
| val result = sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands | ||
| .submitWithResult( | ||
| sv1Backend.config.ledgerApiUser, | ||
| actAs = Seq(dsoParty), | ||
| readAs = Seq(dsoParty), | ||
| update = dsoRules.contractId.exerciseDsoRules_RegisterSynchronizer( | ||
| synchronizerId.toProtoPrimitive, | ||
| operatorParty.toProtoPrimitive, | ||
| ), | ||
| ) | ||
| result.exerciseResult.registeredSynchronizerCid | ||
| } | ||
|
|
||
| val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) | ||
| aliceWalletClient.tap(walletUsdToAmulet(200.0)) | ||
|
|
||
| clue("no traffic is granted before any purchase") { | ||
| extraTrafficLimit(member) shouldBe 0L | ||
| } | ||
|
|
||
| clue("a purchase is granted on the splitwell sequencer") { | ||
| buyTraffic(aliceParty, member, synchronizerId, registration, dsoParty, firstPurchase) | ||
| eventually() { | ||
| extraTrafficLimit(member) shouldBe firstPurchase | ||
| } | ||
| } | ||
|
|
||
| clue("a second purchase raises the limit by exactly its amount") { | ||
| buyTraffic(aliceParty, member, synchronizerId, registration, dsoParty, secondPurchase) | ||
| eventually() { | ||
| extraTrafficLimit(member) shouldBe (firstPurchase + secondPurchase) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private def buyTraffic( | ||
| buyer: PartyId, | ||
| member: Member, | ||
| synchronizerId: SynchronizerId, | ||
| registration: RegisteredSynchronizer.ContractId, | ||
| dsoParty: PartyId, | ||
| trafficAmount: Long, | ||
| )(implicit env: SpliceTestConsoleEnvironment): Unit = { | ||
| val transferContext = | ||
| sv1ScanBackend.getTransferContextWithInstances(CantonTimestamp.now()) | ||
| val amulets = aliceWalletClient.list().amulets.map(_.contract.contractId.contractId) | ||
|
|
||
| aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands | ||
| .submitWithResult( | ||
| aliceValidatorBackend.config.ledgerApiUser, | ||
| actAs = Seq(buyer), | ||
| readAs = Seq(buyer), | ||
| update = transferContext.amuletRules.contract.contractId | ||
| .exerciseAmuletRules_BuyMemberTraffic( | ||
| amulets | ||
| .map[splice.amuletrules.TransferInput](cid => | ||
| new splice.amuletrules.transferinput.InputAmulet( | ||
| new splice.amulet.Amulet.ContractId(cid) | ||
| ) | ||
| ) | ||
| .asJava, | ||
| new splice.amuletrules.TransferContext( | ||
| transferContext.latestOpenMiningRound.contract.contractId, | ||
| Map.empty[Round, IssuingMiningRound.ContractId].asJava, | ||
| Map.empty[String, splice.amulet.ValidatorRight.ContractId].asJava, | ||
| None.toJava, | ||
| ), | ||
| buyer.toProtoPrimitive, | ||
| member.toProtoPrimitive, | ||
| synchronizerId.toProtoPrimitive, | ||
| // a registered synchronizer is pinned to migration id 0 | ||
| 0L, | ||
| trafficAmount, | ||
| Some(dsoParty.toProtoPrimitive).toJava, | ||
| Some(registration).toJava, | ||
| ), | ||
| disclosedContracts = DisclosedContracts | ||
| .forTesting( | ||
| transferContext.amuletRules, | ||
| transferContext.latestOpenMiningRound, | ||
| ) | ||
| .toLedgerApiDisclosedContracts, | ||
| ) | ||
| } | ||
|
|
||
| private def extraTrafficLimit( | ||
| member: Member | ||
| )(implicit env: SpliceTestConsoleEnvironment): Long = | ||
| syncOperatorBackend.appState.sequencerAdminConnection | ||
| .lookupSequencerTrafficControlState(member) | ||
| .futureValue | ||
| .fold(0L)(_.extraTrafficLimit.value) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package org.lfdecentralizedtrust.splice.syncoperator.automation | ||
|
|
||
| import com.digitalasset.canton.config.NonNegativeFiniteDuration | ||
| import com.digitalasset.canton.topology.Member | ||
| import com.digitalasset.canton.tracing.TraceContext | ||
| import io.opentelemetry.api.trace.Tracer | ||
| import org.apache.pekko.stream.Materializer | ||
| import org.lfdecentralizedtrust.splice.automation.{ | ||
| ReconcileSequencerLimitWithMemberTrafficTriggerBase, | ||
| TriggerContext, | ||
| } | ||
| import org.lfdecentralizedtrust.splice.environment.SequencerAdminConnection | ||
| import org.lfdecentralizedtrust.splice.syncoperator.store.SyncOperatorStore | ||
|
|
||
| import scala.concurrent.{ExecutionContext, Future} | ||
|
|
||
| /** Reconciles the traffic purchased for this operator's synchronizer with its sequencer */ | ||
| class ReconcileDedicatedSequencerTrafficTrigger( | ||
| override protected val context: TriggerContext, | ||
| store: SyncOperatorStore, | ||
| sequencerConnection: SequencerAdminConnection, | ||
| trafficBalanceReconciliationDelay: NonNegativeFiniteDuration, | ||
| )(implicit | ||
| ec: ExecutionContext, | ||
| mat: Materializer, | ||
| tracer: Tracer, | ||
| ) extends ReconcileSequencerLimitWithMemberTrafficTriggerBase( | ||
| store, | ||
| trafficBalanceReconciliationDelay, | ||
| ) { | ||
|
|
||
| override protected def sequencerAdminConnection()(implicit | ||
| tc: TraceContext | ||
| ): Future[SequencerAdminConnection] = | ||
| Future.successful(sequencerConnection) | ||
|
|
||
| override protected def getTotalPurchasedMemberTraffic(memberId: Member)(implicit | ||
| tc: TraceContext | ||
| ): Future[Long] = | ||
| store.getTotalPurchasedMemberTraffic(memberId) | ||
|
|
||
| override protected def trafficLimitOffset(memberId: Member)(implicit | ||
| tc: TraceContext | ||
| ): Future[Either[String, Long]] = | ||
| // No prior consumption to carry. | ||
| Future.successful(Right(0L)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,9 @@ import com.digitalasset.canton.console.{ | |
| import com.digitalasset.canton.SynchronizerAlias | ||
| import com.digitalasset.canton.synchronizer.config.SynchronizerParametersConfig | ||
| import com.digitalasset.canton.protocol.DynamicSynchronizerParameters | ||
| import com.digitalasset.canton.admin.api.client.data.TrafficControlParameters | ||
| import com.digitalasset.canton.config.PositiveFiniteDuration | ||
| import com.digitalasset.canton.config.RequireTypes.NonNegativeLong | ||
| import com.digitalasset.canton.topology.transaction.SignedTopologyTransaction.GenericSignedTopologyTransaction | ||
| import com.digitalasset.canton.topology.transaction.TopologyChangeOp | ||
| import com.digitalasset.canton.version.ProtocolVersion | ||
|
|
@@ -36,10 +39,25 @@ def staticParameters(sequencer: LocalInstanceReference) = | |
| .map(StaticSynchronizerParameters(_)) | ||
| .getOrElse(sys.error("whatever")) | ||
|
|
||
| // Canton's own defaults. A member only gets a traffic state, and so can only be granted extra | ||
| // traffic, on a synchronizer that has traffic control enabled. | ||
| val defaultTrafficControlParameters = TrafficControlParameters( | ||
| maxBaseTrafficAmount = NonNegativeLong.tryCreate(10 * 20 * 1024), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought we want to run these without free traffic? |
||
| readVsWriteScalingFactor = PositiveInt.tryCreate(200), | ||
| maxBaseTrafficAccumulationDuration = PositiveFiniteDuration.ofMinutes(10), | ||
| setBalanceRequestSubmissionWindowSize = PositiveFiniteDuration.ofMinutes(5), | ||
| enforceRateLimiting = true, | ||
| baseEventCost = NonNegativeLong.zero, | ||
| freeConfirmationResponses = false, | ||
| ) | ||
|
|
||
| def bootstrapOtherDomain( | ||
| name: String, | ||
| sequencer: LocalSequencerReference, | ||
| mediator: LocalMediatorReference, | ||
| // Traffic control is off by default here: only synchronizers that stand in for a dedicated one | ||
| // need it, and enabling it everywhere would change what every other test sequences against. | ||
| enableTrafficControl: Boolean = false, | ||
| ) = { | ||
| bootstrap.synchronizer( | ||
| name, | ||
|
|
@@ -67,17 +85,19 @@ def bootstrapOtherDomain( | |
| ), | ||
| preparationTimeRecordTimeTolerance = NonNegativeFiniteDuration.ofHours(24), | ||
| mediatorDeduplicationTimeout = NonNegativeFiniteDuration.ofHours(48), | ||
| trafficControl = | ||
| if (enableTrafficControl) Some(defaultTrafficControlParameters) else parameters.trafficControl, | ||
| ), | ||
| signedBy = Some(sequencer.id.uid.namespace.fingerprint), | ||
| // This is test code so just force the change. | ||
| force = ForceFlags(ForceFlag.PreparationTimeRecordTimeToleranceIncrease), | ||
| ) | ||
| } | ||
|
|
||
| Seq( | ||
| ("splitwell", splitwellSequencer, splitwellMediator), | ||
| ("splitwellUpgrade", splitwellUpgradeSequencer, splitwellUpgradeMediator), | ||
| ).foreach((bootstrapOtherDomain _).tupled) | ||
| // splitwell is the only non-global synchronizer a sync operator can be pointed at today, so it | ||
| // carries traffic control; see apps/app/src/test/resources/sync-operator-topology.conf. | ||
| bootstrapOtherDomain("splitwell", splitwellSequencer, splitwellMediator, enableTrafficControl = true) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect this is not gonna work well on CI. Any test that currently uses splitwell might now run out of traffic and will fail. And going back to my earlier comment, if you disable free traffic they definitely fail. I would suggest to make this conditional on some environment variable and then add a dedicated CI job for dedicated synchronizer tests where you set this. that way the existing tests are unaffected. |
||
| bootstrapOtherDomain("splitwellUpgrade", splitwellUpgradeSequencer, splitwellUpgradeMediator) | ||
|
|
||
| // These user allocations are only there | ||
| // for local testing. Our tests allocate their own users. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use actAndCheck instead of clue + eventually