diff --git a/.github/workflows/build.daml_test.yml b/.github/workflows/build.daml_test.yml index 016ba8c4d52..4122ffbdf66 100644 --- a/.github/workflows/build.daml_test.yml +++ b/.github/workflows/build.daml_test.yml @@ -32,7 +32,7 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 8 + cache_version: 10 test_name: daml_test - name: Run Daml tests diff --git a/.github/workflows/build.deployment_test.yml b/.github/workflows/build.deployment_test.yml index a499df219e2..8dd4a07c336 100644 --- a/.github/workflows/build.deployment_test.yml +++ b/.github/workflows/build.deployment_test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: deployment_test with_sbt: false @@ -40,6 +40,11 @@ jobs: cmd_retry_count: 5 # Retry in case of dependency download errors cmd: make cluster/pulumi/test cmd_before_retry: git reset --hard # revert changes before retrying, because make cluster/pulumi/test actually "fixes" the test pulumi files in-place + + - name: Check that pinned docker images are multi-arch + uses: ./.github/actions/nix/run_bash_command_in_nix + with: + cmd: ./scripts/check-multiarch-images.py - name: Report Failures on Slack & Github if: failure() && (github.event_name == 'push' || github.event_name == 'schedule') diff --git a/.github/workflows/build.docs.yml b/.github/workflows/build.docs.yml index 362a4cf0390..3da4bfc3de0 100644 --- a/.github/workflows/build.docs.yml +++ b/.github/workflows/build.docs.yml @@ -25,7 +25,7 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: docs save_nix_cache: true save_nix_cache_to_gcp: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} @@ -48,7 +48,7 @@ jobs: - name: Post-SBT job uses: ./.github/actions/sbt/post_sbt with: - cache_version: 9 + cache_version: 10 setup_sbt_cache_hits: ${{ steps.setup.outputs.sbt_cache_hits }} - name: Report Failures on Slack & Github diff --git a/.github/workflows/build.scala_test.yml b/.github/workflows/build.scala_test.yml index 5f0434d110f..2f0b315b25e 100644 --- a/.github/workflows/build.scala_test.yml +++ b/.github/workflows/build.scala_test.yml @@ -137,7 +137,7 @@ jobs: - name: Run Tests uses: ./.github/actions/tests/scala_test with: - cache_version: 9 + cache_version: 10 with_canton: ${{ inputs.with_canton }} start_canton_options: ${{ inputs.start_canton_options }} test_suite_name: ${{ inputs.test_name }} diff --git a/.github/workflows/build.scala_test_for_compose.yml b/.github/workflows/build.scala_test_for_compose.yml index b348d7723cd..447e46dabb0 100644 --- a/.github/workflows/build.scala_test_for_compose.yml +++ b/.github/workflows/build.scala_test_for_compose.yml @@ -104,7 +104,7 @@ jobs: - name: Run Tests uses: ./.github/actions/tests/scala_test with: - cache_version: 9 + cache_version: 10 start_canton_options: ${{ inputs.start_canton_options }} test_suite_name: ${{ inputs.test_name }} test_names: ${{ needs.split_tests.outputs.test_names }} diff --git a/.github/workflows/build.scala_test_with_cometbft.yml b/.github/workflows/build.scala_test_with_cometbft.yml index 6434e79ee71..bdd9512cf82 100644 --- a/.github/workflows/build.scala_test_with_cometbft.yml +++ b/.github/workflows/build.scala_test_with_cometbft.yml @@ -120,7 +120,7 @@ jobs: - name: Run Tests uses: ./.github/actions/tests/scala_test with: - cache_version: 9 + cache_version: 10 start_canton_options: -F -w test_suite_name: ${{ inputs.test_name }} test_names: ${{ needs.split_tests.outputs.test_names }} diff --git a/.github/workflows/build.static_tests.yml b/.github/workflows/build.static_tests.yml index 1c0a138cca0..8c216bb15b1 100644 --- a/.github/workflows/build.static_tests.yml +++ b/.github/workflows/build.static_tests.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Nix uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: canton_consistency target: 'static_tests' @@ -87,7 +87,7 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: static_tests target: 'static_tests' diff --git a/.github/workflows/build.ts_cli_tests.yml b/.github/workflows/build.ts_cli_tests.yml index 1ddf0f3bf98..edc58dcf4f7 100644 --- a/.github/workflows/build.ts_cli_tests.yml +++ b/.github/workflows/build.ts_cli_tests.yml @@ -41,7 +41,7 @@ jobs: if: steps.skip.outputs.skip != 'true' uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: ts_cli - name: Run Token Standard CLI tests diff --git a/.github/workflows/build.ui_tests.yml b/.github/workflows/build.ui_tests.yml index aa1b5b58557..9000d383a13 100644 --- a/.github/workflows/build.ui_tests.yml +++ b/.github/workflows/build.ui_tests.yml @@ -41,7 +41,7 @@ jobs: if: steps.skip.outputs.skip != 'true' uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: ui_tests - name: Run UI tests @@ -62,7 +62,7 @@ jobs: if: steps.skip.outputs.skip != 'true' uses: ./.github/actions/sbt/post_sbt with: - cache_version: 9 + cache_version: 10 setup_sbt_cache_hits: ${{ steps.setup.outputs.sbt_cache_hits }} - name: Upload logs diff --git a/.github/workflows/bump_gha_runner_version.yml b/.github/workflows/bump_gha_runner_version.yml index b6e3838256a..85d9243b6f9 100644 --- a/.github/workflows/bump_gha_runner_version.yml +++ b/.github/workflows/bump_gha_runner_version.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Nix (Self hosted) uses: ./.github/actions/nix/setup_nix with: - cache_version: 9 + cache_version: 10 target: default - name: Check for the latest version and create a PR to splice diff --git a/.github/workflows/performance_tests.yml b/.github/workflows/performance_tests.yml index caf66cf18d6..0012436b74f 100644 --- a/.github/workflows/performance_tests.yml +++ b/.github/workflows/performance_tests.yml @@ -34,7 +34,7 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: ingestion_performance_tests # Authenticate to GCP for read access to GCS @@ -137,7 +137,7 @@ jobs: id: setup uses: ./.github/actions/tests/common_test_setup with: - cache_version: 9 + cache_version: 10 test_name: read_performance_tests # Authenticate to GCP for read access to GCS diff --git a/.github/workflows/pr_check_github_scripts.yml b/.github/workflows/pr_check_github_scripts.yml index 23f61257972..62b6fe82987 100644 --- a/.github/workflows/pr_check_github_scripts.yml +++ b/.github/workflows/pr_check_github_scripts.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Nix uses: ./.github/actions/nix/setup_nix with: - cache_version: 9 + cache_version: 10 - name: Check github scripts uses: ./.github/actions/nix/run_bash_command_in_nix with: diff --git a/.gitignore b/.gitignore index fae017aaa80..266d64fe72a 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ docs/src/deployment/observability/metrics_reference.rst /.vagrant/ /vagrant-nix-cache/ lnav*work + +# VS Code +.vscode/ diff --git a/Makefile b/Makefile index d63a6e73229..e1315841398 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ $(app-bundle): $(canton-amulet-dar) $(wallet-payments-dar) sbt --client --batch bundle $(canton-amulet-dar) $(wallet-payments-dar) &: - sbt --batch 'splice-amulet-daml'/damlBuild 'splice-wallet-payments-daml'/damlBuild + sbt --client --batch 'splice-amulet-daml/damlBuild; splice-wallet-payments-daml/damlBuild' $(load-tester): cd "${SPLICE_ROOT}/load-tester" && npm ci && npm run build diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/ConfigTransforms.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/ConfigTransforms.scala index 70795113ba7..39ce59e2eb2 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/ConfigTransforms.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/ConfigTransforms.scala @@ -206,6 +206,9 @@ object ConfigTransforms { _.copy(rewardOperationRoundsCloseBufferDuration = NonNegativeFiniteDuration.ofMillis(100)) ), disableDevelopmentFund(), + // Tests default to TrafficBasedAppRewards. Networks (which don't apply + // ConfigTransforms.defaults) fall back to the FeaturedAppMarkers default + withTrafficBasedAppRewards, ) } @@ -795,6 +798,9 @@ object ConfigTransforms { } } + def withNoSvOperationsSwitchOverTimes: ConfigTransform = + updateAllSvAppFoundDsoConfigs_(_.copy(initialSvOperationsSwitchOverTimes = None)) + def withValidatorFaucetCap(cap: BigDecimal): ConfigTransform = updateAllSvAppFoundDsoConfigs_(c => c.copy(optValidatorFaucetCap = Some(cap))) @@ -809,6 +815,24 @@ object ConfigTransforms { ): ConfigTransform = updateAllSvAppFoundDsoConfigs_(c => c.copy(initialRewardConfig = Some(rewardConfig))) + // Tests mint TBAR without dry-run — the network default enables TBAR dry-run + // alongside FeaturedAppMarkers minting, but tests already exercise TBAR + // directly so a dry-run would be redundant. + def withTrafficBasedAppRewards: ConfigTransform = + updateAllSvAppFoundDsoConfigs_(c => + c.copy(initialRewardConfig = + Some( + InitialRewardConfig( + mintingVersion = "RewardVersion_TrafficBasedAppRewards", + dryRunVersion = None, + ) + ) + ) + ) + + def withFeaturedAppMarkers: ConfigTransform = + updateAllSvAppFoundDsoConfigs_(c => c.copy(initialRewardConfig = None)) + private def portTransform(bump: Int, c: AdminServerConfig): AdminServerConfig = c.copy(internalPort = c.internalPort.map(_ + bump)) diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala index 7d76d69eb74..d08e5852a78 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala @@ -35,6 +35,7 @@ import org.lfdecentralizedtrust.splice.sv.SvAppClientConfig import org.lfdecentralizedtrust.splice.sv.config.SvOnboardingConfig.FoundDso import org.lfdecentralizedtrust.splice.util.{ Codec, + IpCidrRateLimits, PerAttributeRateLimitConfig, SpliceRateLimitConfig, } @@ -74,7 +75,14 @@ import com.typesafe.config.ConfigException.UnresolvedSubstitution import org.slf4j.{Logger, LoggerFactory} import pureconfig.configurable.{genericMapReader, genericMapWriter} import pureconfig.generic.{CoproductHint, FieldCoproductHint, ProductHint} -import pureconfig.{ConfigCursor, ConfigReader, ConfigWriter} +import pureconfig.{ + CamelCase, + ConfigCursor, + ConfigFieldMapping, + ConfigReader, + ConfigWriter, + KebabCase, +} import pureconfig.error.{CannotConvert, FailureReason} import pureconfig.module.cats.{nonEmptyListReader, nonEmptyListWriter} import io.circe.parser.* @@ -376,6 +384,15 @@ object SpliceConfig { import pureconfig.generic.semiauto.* + private val perClientIpRateLimitHint: ProductHint[PerAttributeRateLimitConfig] = + ProductHint[PerAttributeRateLimitConfig]( + ConfigFieldMapping { + case "attributeOverrides" => "ip-overrides" + case other => ConfigFieldMapping(CamelCase, KebabCase)(other) + }, + allowUnknownKeys = false, + ) + private val cantonConfigReaders = new CantonConfig.ConfigReaders()(elc) class ConfigReaders(implicit @@ -433,11 +450,22 @@ object SpliceConfig { deriveReader[SpliceParametersConfig] implicit val spliceRateLimiterSimpleConfig: ConfigReader[SpliceRateLimitConfig.Simple] = deriveReader[SpliceRateLimitConfig.Simple] + implicit val clientIpRateLimitHint: ProductHint[PerAttributeRateLimitConfig] = + SpliceConfig.perClientIpRateLimitHint implicit val clientIpRateLimitConfig: ConfigReader[PerAttributeRateLimitConfig] = - deriveReader[PerAttributeRateLimitConfig] - implicit val spliceRateLimiterWithPerClientIpConfig - : ConfigReader[SpliceRateLimitConfig.WithPerClientIp] = - deriveReader[SpliceRateLimitConfig.WithPerClientIp] + deriveReader[PerAttributeRateLimitConfig].emap { config => + Try(IpCidrRateLimits.tryValidate(config)).toEither.left + .map[FailureReason](err => + CannotConvert( + config.attributeOverrides.keys.mkString("[", ", ", "]"), + "ip-overrides", + err.getMessage, + ) + ) + .map(_ => config) + } + implicit val spliceRateLimiterWithPerClientIpConfig: ConfigReader[PerClientIpRateLimitConfig] = + deriveReader[PerClientIpRateLimitConfig] implicit val rateLimitersConfig: ConfigReader[RateLimitersConfig] = deriveReader[RateLimitersConfig] implicit val enabledFeaturesConfigReader: ConfigReader[EnabledFeaturesConfig] = @@ -959,11 +987,12 @@ object SpliceConfig { implicit val spliceRateLimiterSimpleConfig: ConfigWriter[SpliceRateLimitConfig.Simple] = deriveWriter[SpliceRateLimitConfig.Simple] + implicit val clientIpRateLimitHint: ProductHint[PerAttributeRateLimitConfig] = + SpliceConfig.perClientIpRateLimitHint implicit val clientIpRateLimitConfig: ConfigWriter[PerAttributeRateLimitConfig] = deriveWriter[PerAttributeRateLimitConfig] - implicit val spliceRateLimiterWithPerClientIpConfig - : ConfigWriter[SpliceRateLimitConfig.WithPerClientIp] = - deriveWriter[SpliceRateLimitConfig.WithPerClientIp] + implicit val spliceRateLimiterWithPerClientIpConfig: ConfigWriter[PerClientIpRateLimitConfig] = + deriveWriter[PerClientIpRateLimitConfig] implicit val rateLimitersConfig: ConfigWriter[RateLimitersConfig] = deriveWriter[RateLimitersConfig] diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala index dc5c159ef6e..c292314f81c 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala @@ -247,50 +247,6 @@ abstract class ValidatorAppReference( } } - @Help.Summary("Prepare TransferPreapproval send") - def prepareTransferPreapprovalSend( - senderPartyId: PartyId, - receiverPartyId: PartyId, - amount: BigDecimal, - expiresAt: CantonTimestamp, - nonce: Long, - description: Option[String], - verboseHashing: Boolean = false, - ): definitions.PrepareTransferPreapprovalSendResponse = { - consoleEnvironment.run { - httpCommand( - HttpValidatorAdminAppClient.PrepareTransferPreapprovalSend( - senderPartyId, - receiverPartyId, - amount, - expiresAt, - nonce, - description, - verboseHashing, - ) - ) - } - } - - @Help.Summary("Submit TransferPreapproval send") - def submitTransferPreapprovalSend( - senderPartyId: PartyId, - transaction: String, - signature: String, - publicKey: String, - ): String = { - consoleEnvironment.run { - httpCommand( - HttpValidatorAdminAppClient.SubmitTransferPreapprovalSend( - senderPartyId, - transaction, - signature, - publicKey, - ) - ) - } - } - def getExternalPartyBalance(partyId: PartyId): definitions.ExternalPartyBalanceResponse = { consoleEnvironment.run { httpCommand( diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/WalletAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/WalletAppReference.scala index 2e646a636fe..6fea5e9833b 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/WalletAppReference.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/WalletAppReference.scala @@ -709,6 +709,7 @@ abstract class WalletAppReference( amount: BigDecimal, expiresAt: CantonTimestamp, reason: String, + mintAfter: Option[CantonTimestamp] = None, ): AllocateDevelopmentFundCouponResponse = consoleEnvironment.run { httpCommand( @@ -717,6 +718,7 @@ abstract class WalletAppReference( amount, expiresAt, reason, + mintAfter, ) ) } diff --git a/apps/app/src/test/resources/include/mediators.conf b/apps/app/src/test/resources/include/mediators.conf index cec86b612f3..573a40578a4 100644 --- a/apps/app/src/test/resources/include/mediators.conf +++ b/apps/app/src/test/resources/include/mediators.conf @@ -19,10 +19,6 @@ _mediator_template { } topology { validate-initial-topology-snapshot = true - # enable after issue is fixed in canton - enable-topology-state-cache-consistency-checks = true - use-new-processor = true - use-new-client = true } parameters { delayed-verdict-sender { diff --git a/apps/app/src/test/resources/include/participants.conf b/apps/app/src/test/resources/include/participants.conf index eefe33f1c96..7556b891f24 100644 --- a/apps/app/src/test/resources/include/participants.conf +++ b/apps/app/src/test/resources/include/participants.conf @@ -96,9 +96,5 @@ _participant_template { topology { broadcast-batch-size = 1 validate-initial-topology-snapshot = true - # enable after issue is fixed in canton - enable-topology-state-cache-consistency-checks = true - use-new-processor = true - use-new-client = true } } diff --git a/apps/app/src/test/resources/include/sequencers.conf b/apps/app/src/test/resources/include/sequencers.conf index e8bf77998b4..4d220343008 100644 --- a/apps/app/src/test/resources/include/sequencers.conf +++ b/apps/app/src/test/resources/include/sequencers.conf @@ -25,12 +25,6 @@ _sequencer_reference_template { parameters { batching.max-pruning-time-interval = "10 minutes" } - topology { - # enable after issue is fixed in canton - enable-topology-state-cache-consistency-checks = true - use-new-processor = true - use-new-client = true - } sequencer { config { storage = ${_shared.storage} diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala index 041b6cc1e73..c2f3ce1b319 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/config/SpliceConfigTest.scala @@ -86,6 +86,101 @@ class SpliceConfigTest extends AsyncWordSpec with BaseTest { } } + "rate limiting config" should { + + def perClientIpOf(cfg: SpliceConfig) = + cfg.scanApps.values.headOption.value.parameters.rateLimiting.global.perClientIp + + "parse the per client IP CIDR overrides" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.scan-apps.sv1Scan.parameters.rate-limiting.global.per-client-ip { + | enabled = true + | limit.rate-per-second = 10 + | ip-overrides { + | "10.0.0.0/8" = { rate-per-second = 100 } + | "192.0.2.0/24" = { enabled = false, rate-per-second = 0 } + | "10.1.2.3" = { rate-per-second = 200 } + | "2001:db8::/32" = { rate-per-second = 300 } + | "2001:db8:1::/48" = { enabled = false, rate-per-second = 0 } + | "2001:db8:2:3:4:5:6:7" = { rate-per-second = 400 } + | "::/0" = { rate-per-second = 500 } + | } + |} + """.stripMargin + ) + val loaded = + SpliceConfig.loadAndValidate(CantonConfig.mergeConfigs(config, Seq(overwrite))).value + val perClientIp = perClientIpOf(loaded) + perClientIp.limit.ratePerSecond should be(10d) + perClientIp.attributeOverrides.keySet should be( + Set( + "10.0.0.0/8", + "192.0.2.0/24", + "10.1.2.3", + "2001:db8::/32", + "2001:db8:1::/48", + "2001:db8:2:3:4:5:6:7", + "::/0", + ) + ) + perClientIp.attributeOverrides("10.0.0.0/8").ratePerSecond should be(100d) + perClientIp.attributeOverrides("192.0.2.0/24").enabled should be(false) + perClientIp.attributeOverrides("10.1.2.3").ratePerSecond should be(200d) + perClientIp.attributeOverrides("2001:db8::/32").ratePerSecond should be(300d) + perClientIp.attributeOverrides("2001:db8:1::/48").enabled should be(false) + perClientIp.attributeOverrides("2001:db8:2:3:4:5:6:7").ratePerSecond should be(400d) + perClientIp.attributeOverrides("::/0").ratePerSecond should be(500d) + } + + Seq( + "not-an-ip/8", + "10.0.0.0/33", + "10.0.0.256/8", + "2001:db8::/129", + "2001:db8:::1/32", + "not-an-ip/32", + ).foreach { cidr => + s"reject the invalid per client IP CIDR override '$cidr'" in { + val overwrite = ConfigFactory.parseString( + s""" + |canton.scan-apps.sv1Scan.parameters.rate-limiting.rate-limiters.getDsoInfo.per-client-ip { + | ip-overrides { + | "$cidr" = { rate-per-second = 100 } + | } + |} + """.stripMargin + ) + SpliceConfig + .loadAndValidate(CantonConfig.mergeConfigs(config, Seq(overwrite))) + .left + .value + .toString should include(cidr) + } + } + + "reject unknown per client IP keys" in { + val overwrite = ConfigFactory.parseString( + """ + |canton.scan-apps.sv1Scan.parameters.rate-limiting.global.per-client-ip { + | attribute-overrides { + | "10.0.0.0/8" = { rate-per-second = 100 } + | } + |} + """.stripMargin + ) + SpliceConfig + .loadAndValidate(CantonConfig.mergeConfigs(config, Seq(overwrite))) + .left + .value + .toString should include("attribute-overrides") + } + + "default to no per client IP overrides" in { + perClientIpOf(SpliceConfig.loadAndValidate(config).value).attributeOverrides should be(empty) + } + } + // Shared helper for RewardSharingConfig tests private def mkSharingCfg(percentages: BigDecimal*): RewardSharingConfig.BuiltIn = RewardSharingConfig.BuiltIn( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala index a6105b98faf..bf9a45bfb7c 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala @@ -504,13 +504,6 @@ case class EnvironmentDefinition( ) } - def withTransferCommandSupport: EnvironmentDefinition = - this.addConfigTransform((_, conf) => - ConfigTransforms.updateAllValidatorAppConfigs_( - _.copy(enableDeprecatedTransferCommandSupport = true) - )(conf) - ) - def clearConfigTransforms(): EnvironmentDefinition = copy(configTransformsWithContext = _ => Seq()) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AdditionalPackagesToUnvetIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AdditionalPackagesToUnvetIntegrationTest.scala index 750d967f693..6dcaeca8ed7 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AdditionalPackagesToUnvetIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AdditionalPackagesToUnvetIntegrationTest.scala @@ -10,7 +10,6 @@ import org.lfdecentralizedtrust.splice.environment.{DarResource, DarResources} import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest import org.lfdecentralizedtrust.splice.sv.config.SvOnboardingConfig.InitialPackageConfig -import org.lfdecentralizedtrust.splice.util.scalatesttags.RequiresPv35 import org.lfdecentralizedtrust.splice.util.{DarResourcesUtil, PackageUnvettingUtil} abstract class AdditionalPackagesToUnvetIntegrationTestBase @@ -79,96 +78,31 @@ abstract class AdditionalPackagesToUnvetIntegrationTestBase .withManualStart } -/** This test verifies that an SV can unvet packages that still have vetted dependencies - */ -@RequiresPv35 -class PackageWithDependencyIntegrationTest extends AdditionalPackagesToUnvetIntegrationTestBase { - - private val missingDependency = DarResources.wallet_0_1_15 - private val problematicDar = DarResources.walletPayments_0_1_15 - private val darsWithMissingDependency = Seq( - DarResources.wallet_0_1_16 - ) :+ problematicDar - - override val additionalPackagesToUnvetSv1: Seq[DarResource] = darsWithMissingDependency - override val additionalPackagesToUnvetSv1Local: Seq[DarResource] = - darsWithMissingDependency :+ missingDependency - - "sv1 can unvet packages that still have dependencies" in { implicit env => - import env.executionContext - - startAllSync( - sv1Backend, - sv1ScanBackend, - sv1ValidatorBackend, - ) - val synchronizerId = - sv1Backend.participantClient.synchronizers.list_connected().head.synchronizerId - - clue(s"sv1 can unvet a package if dependencies to it remains, additionalPackagesToUnvet: ${additionalPackagesToUnvetSv1 - .map(pkg => pkg.metadata.name -> pkg.metadata.version)}") { - eventually() { - val vettedPackageIds = getVettedPackageIds( - sv1Backend.appState.participantAdminConnection, - synchronizerId, - ) - vettedPackageIds should contain noElementsOf darsWithMissingDependency.map(_.packageId) - vettedPackageIds should contain(missingDependency.packageId) - } - stopAllAsync( - sv1Backend, - sv1ValidatorBackend, - ).futureValue - } - - clue( - s"sv1 can unvet a package if all dependencies to it are unvetted as well, additionalPackagesToUnvet: ${additionalPackagesToUnvetSv1Local - .map(pkg => pkg.metadata.name -> pkg.metadata.version)}" - ) { - startAllSync( - sv1LocalBackend, - sv1ValidatorLocalBackend, - ) - eventually() { - val vettedPackageIds = getVettedPackageIds( - sv1LocalBackend.appState.participantAdminConnection, - synchronizerId, - ) - vettedPackageIds should contain noElementsOf darsWithMissingDependency :+ missingDependency - } - stopAllAsync( - sv1LocalBackend, - sv1ValidatorLocalBackend, - ).futureValue - } - } -} - /** This test verifies that an SV can downgrade to the versions before an upgrade. */ class DowngradeSvPackagesIntegrationTest extends AdditionalPackagesToUnvetIntegrationTestBase { - private val darsFromUpgrade_0_5_16 = Seq( - DarResources.amulet_0_1_17, - DarResources.amuletNameService_0_1_18, - DarResources.dsoGovernance_0_1_23, - DarResources.walletPayments_0_1_17, - DarResources.wallet_0_1_18, - DarResources.amulet_0_1_17, + // Use versions one above minimumInitialization so that after unvetting, + // the minimum versions still remain vetted and the system can function. + private val darsToDowngradeTo = Seq( + DarResources.amulet_0_1_20, + DarResources.amuletNameService_0_1_21, + DarResources.dsoGovernance_0_1_26, + DarResources.walletPayments_0_1_20, + DarResources.wallet_0_1_21, ) - // initialize network with the versions from the upgrade 0.5.16 override val initialPackageConfig: InitialPackageConfig = new InitialPackageConfig( - DarResources.amulet_0_1_17.metadata.version.toString(), - DarResources.amuletNameService_0_1_18.metadata.version.toString(), - DarResources.dsoGovernance_0_1_23.metadata.version.toString(), + DarResources.amulet_0_1_20.metadata.version.toString(), + DarResources.amuletNameService_0_1_21.metadata.version.toString(), + DarResources.dsoGovernance_0_1_26.metadata.version.toString(), DarResources.validatorLifecycle_0_1_6.metadata.version.toString(), - DarResources.wallet_0_1_18.metadata.version.toString(), - DarResources.walletPayments_0_1_17.metadata.version.toString(), + DarResources.wallet_0_1_21.metadata.version.toString(), + DarResources.walletPayments_0_1_20.metadata.version.toString(), ) - override val additionalPackagesToUnvetSv1: Seq[DarResource] = darsFromUpgrade_0_5_16 + override val additionalPackagesToUnvetSv1: Seq[DarResource] = darsToDowngradeTo override val additionalPackagesToUnvetSv1Local: Seq[DarResource] = Seq( - DarResources.wallet_0_1_16 + DarResources.wallet_0_1_20 ) "sv1 can unvet all upgraded sv packages" in { implicit env => @@ -188,7 +122,7 @@ class DowngradeSvPackagesIntegrationTest extends AdditionalPackagesToUnvetIntegr getVettedPackageIds( sv1Backend.appState.participantAdminConnection, synchronizerId, - ) should contain noElementsOf darsFromUpgrade_0_5_16.map(_.packageId) + ) should contain noElementsOf darsToDowngradeTo.map(_.packageId) } stopAllAsync( sv1Backend, @@ -209,8 +143,8 @@ class DowngradeSvPackagesIntegrationTest extends AdditionalPackagesToUnvetIntegr sv1LocalBackend.appState.participantAdminConnection, synchronizerId, ) - vettedPackageIds should contain allElementsOf darsFromUpgrade_0_5_16.map(_.packageId) - vettedPackageIds should not contain DarResources.wallet_0_1_16.packageId + vettedPackageIds should contain allElementsOf darsToDowngradeTo.map(_.packageId) + vettedPackageIds should not contain DarResources.wallet_0_1_19.packageId } stopAllAsync( sv1LocalBackend, @@ -225,6 +159,10 @@ class DowngradeSvPackagesIntegrationTest extends AdditionalPackagesToUnvetIntegr class UnvetAllSupportedPackagesIntegrationTest extends AdditionalPackagesToUnvetIntegrationTestBase { + override def environmentDefinition = super.environmentDefinition.addConfigTransform((_, c) => + ConfigTransforms.withNoSvOperationsSwitchOverTimes(c) + ) + private val minimalPackageVersions = DarResourcesUtil.minimalPackageVersions private val nonMinimalSupportedPackageVersions = DarResourcesUtil.supportedPackageVersions.filterNot(minimalPackageVersions.contains(_)) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala index 2e44c62ddc4..5d64d3104dd 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala @@ -34,8 +34,6 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.AmuletRul import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.amuletrules_actionrequiringconfirmation.CRARC_SetConfig import scala.jdk.CollectionConverters.* -import java.time.Instant -import java.time.temporal.ChronoUnit import scala.util.Using import scala.util.Using.Releasable import scala.concurrent.duration.* @@ -230,12 +228,6 @@ class AppUpgradeIntegrationTest val amuletRules = sv2ScanBackend.getAmuletRules() val amuletConfig = amuletRules.payload.configSchedule.initialValue - // Ideally we'd like the config to take effect immediately. However, we - // can only schedule configs in the future and this is enforced at the Daml level. - // So we pick a date that is far enough in the future that we can complete the voting process - // before it is reached but close enough that we don't need to wait for long. - // 12 seconds seems to work well empirically. - val scheduledTime = Instant.now().plus(12, ChronoUnit.SECONDS) val newAmuletConfig = new splice.amuletconfig.AmuletConfig( SpliceUtil.defaultTransferConfig( amuletConfig.transferConfig.maxNumInputs, @@ -258,6 +250,9 @@ class AppUpgradeIntegrationTest amuletConfig.externalPartyConfigStateTickDuration, amuletConfig.rewardConfig, amuletConfig.transferPreapprovalBaseDuration, + amuletConfig.developmentFundManagerBlacklist, + amuletConfig.minDevelopmentFundMintingDelay, + amuletConfig.amuletSwitchOverTimes, ) val upgradeAction = new ARC_AmuletRules( new CRARC_SetConfig( @@ -305,15 +300,19 @@ class AppUpgradeIntegrationTest }, ) - // Ensure that the code below really uses the new version. Locally things can be sufficiently - // fast that you otherwise still end up using the old version. - env.environment.clock - .scheduleAt( - _ => (), - CantonTimestamp.assertFromInstant(scheduledTime.plus(500, ChronoUnit.MILLIS)), - ) - .unwrap - .futureValue + clue("SVs have vetted new dso governance version") { + eventually() { + // dso party vetting only changes once all SVs vetted the package. + val preferredPackages = + sv1Backend.participantClientWithAdminToken.ledger_api.interactive_submission + .preferred_packages( + Map(DarResources.dsoGovernance_current.metadata.name -> Set(dsoParty)) + ) + val dsoGovernancePackage = preferredPackages.packageReferences.loneElement + dsoGovernancePackage.packageName shouldBe DarResources.dsoGovernance_current.metadata.name.toString + dsoGovernancePackage.packageVersion shouldBe DarResources.dsoGovernance_current.metadata.version.toString + } + } // Vote on a dummy change on amulet rules to ensure it is archived and recreated // which indicates the new choice is being used. diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BatchedFeaturedAppActivityMarkerIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BatchedFeaturedAppActivityMarkerIntegrationTest.scala index e09a0cc265a..83c15d379c8 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BatchedFeaturedAppActivityMarkerIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BatchedFeaturedAppActivityMarkerIntegrationTest.scala @@ -41,13 +41,16 @@ class BatchedFeaturedAppActivityMarkerIntegrationTest _.withPausedTrigger[FeaturedAppActivityMarkerTrigger] )(config) ) - .addConfigTransforms((_, config) => - ConfigTransforms.updateAllSvAppConfigs_( - _.copy( // Set high batch sizes because we really want to test that everything gets submitted in a single batch - delegatelessAutomationFeaturedAppActivityMarkerCatchupThreshold = 500, - delegatelessAutomationFeaturedAppActivityMarkerBatchSize = 500, - ) - )(config) + .addConfigTransforms( + (_, config) => + ConfigTransforms.updateAllSvAppConfigs_( + _.copy( // Set high batch sizes because we really want to test that everything gets submitted in a single batch + delegatelessAutomationFeaturedAppActivityMarkerCatchupThreshold = 500, + delegatelessAutomationFeaturedAppActivityMarkerBatchSize = 500, + ) + )(config), + // Disable so downgrades of dso rules don't cause issues with validators where we deliberately unvet versions. + (_, c) => ConfigTransforms.withNoSvOperationsSwitchOverTimes(c), ) def createBatchedMarkers( @@ -130,14 +133,14 @@ class BatchedFeaturedAppActivityMarkerIntegrationTest val amuletVersion = sv1ScanBackend.getAmuletRules().payload.configSchedule.initialValue.packageConfig.amulet // check that the test actually uses a different version - amuletVersion should not be "0.1.15" - clue("Bob unvets amulet > 0.1.15") { + amuletVersion should not be "0.1.19" + clue("Bob unvets amulet > 0.1.19") { bobValidatorBackend.validatorAutomation .trigger[ValidatorPackageVettingTrigger] .pause() .futureValue val packagesToUnvet = DarResources.amulet.others.filter( - _.metadata.version > DarResources.amulet_0_1_15.metadata.version + _.metadata.version > DarResources.amulet_0_1_19.metadata.version ) logger.info(s"Unvetting ${packagesToUnvet.map(d => (d.metadata.version, d.packageId))}") bobValidatorBackend.participantClient.topology.vetted_packages.propose_delta( @@ -268,7 +271,7 @@ class BatchedFeaturedAppActivityMarkerIntegrationTest ), entries => { forExactly(1, entries) { line => - line.message should (include("vettedAmuletVersion = 0.1.15") and include("Processing")) + line.message should (include("vettedAmuletVersion = 0.1.19") and include("Processing")) } forAtLeast(1, entries) { line => line.message should (include( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala index 636cee7873c..5495102a183 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala @@ -241,6 +241,9 @@ class BootstrapPackageConfigIntegrationTest amuletConfig.externalPartyConfigStateTickDuration, amuletConfig.rewardConfig, amuletConfig.transferPreapprovalBaseDuration, + amuletConfig.developmentFundManagerBlacklist, + amuletConfig.minDevelopmentFundMintingDelay, + amuletConfig.amuletSwitchOverTimes, ) val upgradeAction = new ARC_AmuletRules( @@ -391,6 +394,9 @@ class BootstrapPackageConfigIntegrationTest amuletConfig.externalPartyConfigStateTickDuration, amuletConfig.rewardConfig, amuletConfig.transferPreapprovalBaseDuration, + amuletConfig.developmentFundManagerBlacklist, + amuletConfig.minDevelopmentFundMintingDelay, + amuletConfig.amuletSwitchOverTimes, ) val upgradeAction = new ARC_AmuletRules( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapTest.scala index 3a4d5777f92..c7ce88e123a 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapTest.scala @@ -20,6 +20,9 @@ class BootstrapTest extends IntegrationTestWithIsolatedEnvironment { this.getClass.getSimpleName, ) .clearConfigTransforms() + // clearConfigTransforms skips ConfigTransforms.defaults, so we re-apply + // the TBAR configuration explicitly here + .addConfigTransform((_, config) => ConfigTransforms.withTrafficBasedAppRewards(config)) .addConfigTransforms((_, config) => ConfigTransforms.withPausedSvDomainComponentsOffboardingTriggers()(config) ) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala index dd67011e5be..6af49861d64 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/CryptoHashEquivalenceIntegrationTest.scala @@ -148,6 +148,7 @@ class CryptoHashEquivalenceIntegrationTest extends IntegrationTest with WalletTe updateHistory, DbAppActivityRecordStore.IngestionVersions(1, 0), isFirstSv = false, + initialRound = 0L, loggerFactory, ) rewardsStore = new DbScanAppRewardsStore( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala index a8a1bae6949..aca66ca2a66 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundCouponIntegrationTest.scala @@ -433,6 +433,90 @@ class DevelopmentFundCouponIntegrationTest } } + "Minting a development fund coupon once its mintAfter has passed" in { implicit env => + onboardWalletUser(aliceValidatorWalletClient, aliceValidatorBackend) + val sv1UserId = sv1WalletClient.config.ledgerApiUser + val bobParty = onboardWalletUser(bobWalletClient, bobValidatorBackend) + val beneficiary = bobParty + val initialUnclaimedDevelopmentFundCouponAmount = BigDecimal(SpliceUtil.damlDecimal(1000)) + val developmentFundCouponAmount = BigDecimal(SpliceUtil.damlDecimal(40.0)) + val expiresAt = CantonTimestamp.now().plus(Duration.ofDays(1)) + val reason = "Bob has contributed to the Daml repo" + val mintingDelay = Duration.ofSeconds(10) + + val bobUserName = bobWalletClient.config.ledgerApiUser + val bobMergeAmuletsTrigger = + bobValidatorBackend + .userWalletAutomation(bobUserName) + .futureValue + .trigger[CollectRewardsAndMergeAmuletsTrigger] + + val unclaimedTotalBefore = getUnclaimedDevelopmentFundCouponTotal(sv1ValidatorBackend) + actAndCheck( + "Mint one unclaimed development fund coupon", { + createUnclaimedDevelopmentFundCoupon( + sv1ValidatorBackend.participantClientWithAdminToken, + sv1UserId, + initialUnclaimedDevelopmentFundCouponAmount, + ) + }, + )( + "The unclaimed development fund coupon is created", + _ => { + getUnclaimedDevelopmentFundCouponTotal( + sv1ValidatorBackend + ) shouldBe (unclaimedTotalBefore + initialUnclaimedDevelopmentFundCouponAmount) + }, + ) + + val bobBalanceBefore = bobWalletClient.balance().unlockedQty + setTriggersWithin( + triggersToPauseAtStart = Seq(bobMergeAmuletsTrigger) + ) { + actAndCheck( + "Allocate one development fund coupon that is not mintable yet", { + val mintAfter = CantonTimestamp.now().plus(mintingDelay) + aliceValidatorWalletClient.allocateDevelopmentFundCoupon( + beneficiary, + developmentFundCouponAmount, + expiresAt, + reason, + Some(mintAfter), + ) + mintAfter + }, + )( + "The coupon is created and carries the requested mintAfter", + allocatedMintAfter => { + val coupons = bobWalletClient.listActiveDevelopmentFundCoupons() + coupons should have size 1 withClue "bob coupons" + coupons.head.payload.mintAfter shouldBe java.util.Optional.of( + allocatedMintAfter.toInstant + ) + }, + ) + } + + clue("The coupon is collected once its mintAfter has passed") { + eventually(30.seconds) { + bobWalletClient + .listActiveDevelopmentFundCoupons() shouldBe empty withClue "bob coupons after mintAfter" + bobWalletClient.balance().unlockedQty shouldBe + (bobBalanceBefore + developmentFundCouponAmount) + } + } + + clue("The collected coupon is listed in listDevelopmentFundCouponHistory as claimed") { + eventually() { + assertListDevelopmentFundCouponHistoryStatuses( + bobWalletClient, + beneficiary, + Seq(httpDef.ArchivedDevelopmentFundCoupon.Status.Claimed -> None), + ) + } + } + } + "Expiring a development fund coupon" in { implicit env => val sv1UserId = sv1WalletClient.config.ledgerApiUser onboardWalletUser(aliceValidatorWalletClient, aliceValidatorBackend) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala index 38afbe9663b..0b6c9bf87b3 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala @@ -119,6 +119,8 @@ class DevelopmentFundFrontendTimeBasedIntegrationTest val futureExpiresAtFormatted = formatDateTimeForUI(latestTime.plus(Duration.ofDays(365 * 30))) + val mintAfterInstant = latestTime.plus(Duration.ofDays(10)) + val mintAfterFormatted = formatDateTimeForUI(mintAfterInstant) // =================================================================== // Section: Create coupons for user_1, change DFM, and verify transition @@ -250,6 +252,16 @@ class DevelopmentFundFrontendTimeBasedIntegrationTest "development-fund-allocation-expires-at", futureExpiresAtFormatted, ) + waitForQuery(id("development-fund-allocation-mint-after")) + webDriver + .findElement( + org.openqa.selenium.By.id("development-fund-allocation-mint-after") + ) + .getAttribute("value") shouldBe empty + setDateTimeWithoutScroll( + "development-fund-allocation-mint-after", + mintAfterFormatted, + ) eventuallyClickOn(id("development-fund-allocation-reason")) textArea(id("development-fund-allocation-reason")).underlying.sendKeys( "Coupon 3 - stays active" @@ -257,13 +269,31 @@ class DevelopmentFundFrontendTimeBasedIntegrationTest eventuallyClickOn(id("development-fund-allocation-submit-button")) }, )( - "Coupon 3 is allocated", + "Coupon 3 is allocated carrying the mint delay entered in the form", _ => { eventually() { - aliceWalletClient.listActiveDevelopmentFundCoupons() should have size 1 + val coupons = aliceWalletClient.listActiveDevelopmentFundCoupons() + coupons should have size 1 + val couponMintAfter = coupons.head.payload.mintAfter.toScala.value + couponMintAfter.isAfter( + mintAfterInstant.minus(Duration.ofDays(1)) + ) shouldBe true withClue "mintAfter lower bound" + couponMintAfter.isBefore( + mintAfterInstant.plus(Duration.ofDays(1)) + ) shouldBe true withClue "mintAfter upper bound" } }, ) + + clue("Check: the active coupon row renders its Mint After") { + eventually() { + val mintAfterCells = + findAll(cssSelector("#active-coupons-table tbody tr td:nth-child(5)")).toSeq + mintAfterCells should have size 1 + mintAfterCells.head.text should fullyMatch regex + """[A-Z][a-z]{2} \d{1,2}, \d{4} \d{2}:\d{2} [AP]M""" + } + } } } @@ -374,7 +404,7 @@ class DevelopmentFundFrontendTimeBasedIntegrationTest clue("Check: user_2's Active List is empty") { eventually() { val emptyStateCell = find( - cssSelector("#active-coupons-table tbody tr td[colspan='6']") + cssSelector("#active-coupons-table tbody tr td[colspan='7']") ) emptyStateCell.isDefined shouldBe true emptyStateCell.value.text should include("No development fund allocations found") @@ -446,6 +476,9 @@ class DevelopmentFundFrontendTimeBasedIntegrationTest existingConfig.externalPartyConfigStateTickDuration, existingConfig.rewardConfig, existingConfig.transferPreapprovalBaseDuration, + existingConfig.developmentFundManagerBlacklist, + existingConfig.minDevelopmentFundMintingDelay, + existingConfig.amuletSwitchOverTimes, ) val action = new ARC_AmuletRules( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala index 5211596688a..f507747dadf 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExpiryWithMinimalVettedPackagesIntegrationTest.scala @@ -77,6 +77,8 @@ abstract class ExpiryWithMinimalVettedPackagesIntegrationTestBase ConfigTransforms.updateInitialExternalPartyConfigStateTickDuration( NonNegativeFiniteDuration.ofMillis(500) )(c), + // The validator on the old version otherwise will fail to onboard as we cannot downgrade DsoRules when trying to create their license. + (_, c) => ConfigTransforms.withNoSvOperationsSwitchOverTimes(c), ) .addConfigTransforms((_, config) => { val aliceVal = InstanceName.tryCreate("aliceValidator") @@ -265,8 +267,9 @@ class AmuletExpiryV1FallbackIntegrationTest class ExpiryWithIgnoredAmuletVersionIntegrationTest extends ExpiryWithMinimalVettedPackagesIntegrationTestBase { + // Amulet version 0.1.19 is just below the minimumInitialization version. override val ignoredAmuletVersions: Set[String] = Set( - DarResources.amulet_0_1_15.metadata.version.toString + DarResources.amulet_0_1_19.metadata.version.toString ) private val entryName = "alice.unverified.ans" @@ -420,33 +423,23 @@ class ExpiryWithNoVettedAmuletVersionIntegrationTest } } - loggerFactory.assertLogsSeq( - SuppressionRule.forLogger[ExpiredAmuletTrigger] && SuppressionRule.Level(Level.WARN) + actAndCheck(timeUntilSuccess = 60.seconds)( + "Advance 4 rounds and resume the amulet expiry trigger", { + (1 to 4).foreach(_ => advanceRoundsByOneTickViaAutomation()) + updateExternalPartyConfigStatesViaAutomation() + updateExternalPartyConfigStatesViaAutomation() + env.svs.local.foreach( + _.dsoDelegateBasedAutomation.trigger[ExpiredAmuletTrigger].resume() + ) + }, )( - actAndCheck(timeUntilSuccess = 60.seconds)( - "Advance 4 rounds and resume the amulet expiry trigger", { - (1 to 4).foreach(_ => advanceRoundsByOneTickViaAutomation()) - updateExternalPartyConfigStatesViaAutomation() - updateExternalPartyConfigStatesViaAutomation() - env.svs.local.foreach( - _.dsoDelegateBasedAutomation.trigger[ExpiredAmuletTrigger].resume() - ) - }, - )( - "Alice is ignored and her dust amulets are not expired", - _ => { - val ignored = sv1Backend.dsoDelegateBasedAutomation.unavailablePartiesStore.getAll - ignored should contain(alice) - ignored should not contain dsoParty - aliceWalletClient.list().amulets should have length 2L withClue "dust amulets" - }, - ), - entries => - forAtLeast(1, entries) { entry => - entry.warningMessage should include("No vetted Amulet version") - entry.warningMessage should include(alice.uid.identifier.str) - entry.warningMessage should include("ignoring 1 parties") - }, + "Alice is ignored and her dust amulets are not expired", + _ => { + val ignored = sv1Backend.dsoDelegateBasedAutomation.unavailablePartiesStore.getAll + ignored should contain(alice) + ignored should not contain dsoParty + aliceWalletClient.list().amulets should have length 2L withClue "dust amulets" + }, ) } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala index 6ab887197ca..fbbc0dfca3a 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala @@ -6,7 +6,6 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{ ExternalPartySetupProposal, TransferPreapproval, } -import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletrules.TransferCommand import org.lfdecentralizedtrust.splice.codegen.java.splice.round.IssuingMiningRound import org.lfdecentralizedtrust.splice.codegen.java.splice.types.Round import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.install.amuletoperation.CO_CreateExternalPartySetupProposal @@ -20,8 +19,6 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ ConfigurableApp, updateAutomationConfig, } -import org.lfdecentralizedtrust.splice.http.v0.definitions -import definitions.DamlValueEncoding.members.CompactJson import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTestWithIsolatedEnvironment, @@ -33,20 +30,13 @@ import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ ExpireTransferPreapprovalsTrigger, } import org.lfdecentralizedtrust.splice.util.{DisclosedContracts, TriggerTestUtil, WalletTestUtil} -import org.lfdecentralizedtrust.splice.validator.automation.{ - RenewTransferPreapprovalTrigger, - TransferCommandSendTrigger, -} -import com.daml.ledger.javaapi.data.codegen.json.JsonLfReader +import org.lfdecentralizedtrust.splice.validator.automation.RenewTransferPreapprovalTrigger import com.digitalasset.canton.HasExecutionContext import com.digitalasset.canton.config.NonNegativeFiniteDuration -import com.digitalasset.canton.crypto.* -import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.topology.PartyId -import com.digitalasset.canton.util.HexString import monocle.macros.syntax.lens.* -import java.time.{Duration, Instant} +import java.time.Instant import java.util.UUID import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* @@ -75,6 +65,8 @@ class ExternalPartySetupProposalIntegrationTest override def environmentDefinition: EnvironmentDefinition = { EnvironmentDefinition .simpleTopology1Sv(this.getClass.getSimpleName) + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) .addConfigTransforms( // set renewal duration to be same as pre-approval lifetime to ensure renewal // gets triggered immediately @@ -103,7 +95,6 @@ class ExternalPartySetupProposalIntegrationTest NonNegativeFiniteDuration.ofMillis(500) )(config), ) - .withTransferCommandSupport } @@ -266,330 +257,6 @@ class ExternalPartySetupProposalIntegrationTest c => c.data.provider == aliceParty.toProtoPrimitive, ) should have length (1), ) - - // Transfer 1000.0 from Alice to Bob - val prepareSend = - aliceValidatorBackend.prepareTransferPreapprovalSend( - aliceParty, - bobParty, - BigDecimal(1000.0), - CantonTimestamp.now().plus(Duration.ofHours(24)), - 0L, - Some("transfer-command-description"), - verboseHashing = true, - ) - prepareSend.hashingDetails should not be empty withClue "TransferPreapproval send hashingDetails" - val (updateId, _) = actAndCheck( - "Submit signed TransferCommand creation", - aliceValidatorBackend.submitTransferPreapprovalSend( - aliceParty, - prepareSend.transaction, - HexString.toHexString( - crypto - .signBytes( - HexString.parseToByteString(prepareSend.txHash).value, - alicePrivateKey.asInstanceOf[SigningPrivateKey], - usage = SigningKeyUsage.ProtocolOnly, - ) - .value - .toProtoV30 - .signature - ), - publicKeyAsHexString(alicePublicKey), - ), - )( - "validator automation completes transfer", - _ => { - BigDecimal( - aliceValidatorBackend - .getExternalPartyBalance(aliceParty) - .totalUnlockedCoin - ) should be( - BigDecimal(1000) + BigDecimal( - issuingRound.issuancePerUnfeaturedAppRewardCoupon - ) * appRewardAmount - ) - bobValidatorBackend - .getExternalPartyBalance(bobParty) - .totalUnlockedCoin shouldBe "1000.0000000000" - aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(amuletCodegen.AppRewardCoupon.COMPANION)( - aliceParty, - c => c.data.provider == aliceParty.toProtoPrimitive, - ) shouldBe empty withClue "alice AppRewardCoupons" - // Transfer command counter gets created/incremented - aliceValidatorBackend.scanProxy - .lookupTransferCommandCounterByParty(aliceParty) - .value - .payload - .nextNonce shouldBe 1 - val result = aliceValidatorBackend.scanProxy - .lookupTransferCommandStatus( - aliceParty, - 0L, - ) - .value - result.transferCommandsByContractId.values.loneElement.status shouldBe definitions.TransferCommandContractStatus.members - .TransferCommandSentResponse( - definitions.TransferCommandSentResponse(status = "sent") - ) - result.transferCommandsByContractId.keys.loneElement should startWith( - prepareSend.transferCommandContractIdPrefix - ) - val payload = TransferCommand - .jsonDecoder() - .decode( - new JsonLfReader( - result.transferCommandsByContractId.values.loneElement.contract.payload.noSpaces - ) - ) - payload shouldBe new TransferCommand( - dsoParty.toProtoPrimitive, - aliceParty.toProtoPrimitive, - bobParty.toProtoPrimitive, - aliceValidatorBackend.getValidatorPartyId().toProtoPrimitive, - BigDecimal(1000.0).bigDecimal, - payload.expiresAt, - 0L, - java.util.Optional.of("transfer-command-description"), - ) - }, - ) - val update = eventuallySucceeds() { - sv1ScanBackend.getUpdate(updateId, encoding = CompactJson) - } - // Create a validator reward to test reward minting - val (_, rewardRound) = actAndCheck( - "Create validator reward", - createRewards( - appRewards = Seq.empty, - validatorRewards = Seq((aliceParty, BigDecimal(1.0))), - ), - )( - "reward is observable", - _ => - aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(amuletCodegen.ValidatorRewardCoupon.COMPANION)( - aliceParty, - c => c.data.user == aliceParty.toProtoPrimitive, - ) - .loneElement - .data - .round - .number, - ) - - actAndCheck( - s"Advance rounds until $rewardRound is issuing", { - advanceRoundsByOneTickViaAutomation() - advanceRoundsByOneTickViaAutomation() - advanceRoundsByOneTickViaAutomation() - }, - )( - s"Round $rewardRound is issuing", - _ => { - val (_, issuingRounds) = sv1ScanBackend.getOpenAndIssuingMiningRounds() - issuingRounds.map(_.payload.round.number) should contain(rewardRound) - }, - ) - clue("ValidatorRewardCoupon gets collected") { - eventually() { - // Just checking for archival. Checking the tx history or something for collection is a bit annoying since there - // are multiple things resulting in validator rewards and we only get a total sum. - aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(amuletCodegen.ValidatorRewardCoupon.COMPANION)( - aliceParty, - c => c.data.user == aliceParty.toProtoPrimitive, - ) shouldBe empty withClue "alice ValidatorRewardCoupons" - - // Sanity check that the reward really got collected and not expired. - val (_, issuingRounds) = sv1ScanBackend.getOpenAndIssuingMiningRounds() - issuingRounds.map(_.payload.round.number) should contain(rewardRound) - } - } - - inside(update) { - case definitions.UpdateHistoryItem.members.UpdateHistoryTransaction(transaction) => - forExactly(1, transaction.eventsById) { - case (_, definitions.TreeEvent.members.ExercisedEvent(ev)) => - ev.choice shouldBe "ExternalPartyAmuletRules_CreateTransferCommand" - case _ => fail() - } - } - - // Check that transfer works correctly with featured app rights - bobValidatorWalletClient.selfGrantFeaturedAppRight() - // Transfer 500.0 from Alice to Bob - val prepareSendFeatured = - aliceValidatorBackend.prepareTransferPreapprovalSend( - aliceParty, - bobParty, - BigDecimal(500.0), - CantonTimestamp.now().plus(Duration.ofHours(24)), - 1L, - Some("transfer-command-description"), - verboseHashing = true, - ) - prepareSendFeatured.hashingDetails should not be empty withClue "TransferPreapproval send hashingDetails" - val (_, _) = actAndCheck( - "Submit signed TransferCommand creation", - aliceValidatorBackend.submitTransferPreapprovalSend( - aliceParty, - prepareSendFeatured.transaction, - HexString.toHexString( - crypto - .signBytes( - HexString.parseToByteString(prepareSendFeatured.txHash).value, - alicePrivateKey.asInstanceOf[SigningPrivateKey], - usage = SigningKeyUsage.ProtocolOnly, - ) - .value - .toProtoV30 - .signature - ), - publicKeyAsHexString(alicePublicKey), - ), - )( - "validator automation completes transfer", - _ => { - BigDecimal( - aliceValidatorBackend - .getExternalPartyBalance(aliceParty) - .totalUnlockedCoin - ) should be( - BigDecimal( - 2000 - 1000 - 500 - ) + BigDecimal(issuingRound.issuancePerUnfeaturedAppRewardCoupon) * appRewardAmount - ) - bobValidatorBackend - .getExternalPartyBalance(bobParty) - .totalUnlockedCoin shouldBe "1500.0000000000" - val rewards = - bobValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(amuletCodegen.AppRewardCoupon.COMPANION)( - bobValidatorBackend.getValidatorUserInfo().primaryParty, - c => - c.data.provider == bobValidatorBackend - .getValidatorUserInfo() - .primaryParty - .toProtoPrimitive, - ) - rewards.loneElement.data.featured shouldBe true - }, - ) - - // Check that transfer command gets archived if preapproval does not exist. - val sv1Party = sv1Backend.getDsoInfo().svParty - val now = env.environment.clock.now.toInstant - // Create a preapproval temporarily, otherwise the prepare step already rejects - val (preapproval, _) = actAndCheck( - "Create preapproval", - sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitWithResult( - userId = sv1Backend.config.ledgerApiUser, - actAs = Seq(dsoParty, sv1Party), - readAs = Seq.empty, - update = new TransferPreapproval( - dsoParty.toProtoPrimitive, - sv1Party.toProtoPrimitive, - sv1Party.toProtoPrimitive, - now, - now, - now.plusMillis(500), - ).create, - ), - )( - "Preapproval is ingested by scan", - _ => - inside(aliceValidatorBackend.scanProxy.lookupTransferPreapprovalByParty(sv1Party)) { - case Some(_) => - succeed - }, - ) - - val prepareSendNoPreapproval = - aliceValidatorBackend.prepareTransferPreapprovalSend( - aliceParty, - sv1Party, - BigDecimal(10.0), - CantonTimestamp.now().plus(Duration.ofHours(24)), - 2L, - Some("transfer-command-description"), - ) - prepareSendNoPreapproval.hashingDetails shouldBe empty withClue "TransferPreapproval send hashingDetails" - - // Archive the preapproval - sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands - .submitWithResult( - userId = sv1Backend.config.ledgerApiUser, - actAs = Seq(dsoParty, sv1Party), - readAs = Seq.empty, - update = preapproval.contractId.exerciseArchive(), - ) - setTriggersWithin(triggersToPauseAtStart = - Seq(aliceValidatorBackend.validatorAutomation.trigger[TransferCommandSendTrigger]) - ) { - val (_, suffixedCid) = actAndCheck( - "Submit signed TransferCommand creation", - aliceValidatorBackend.submitTransferPreapprovalSend( - aliceParty, - prepareSendNoPreapproval.transaction, - HexString.toHexString( - crypto - .signBytes( - HexString.parseToByteString(prepareSendNoPreapproval.txHash).value, - alicePrivateKey.asInstanceOf[SigningPrivateKey], - usage = SigningKeyUsage.ProtocolOnly, - ) - .value - .toProtoV30 - .signature - ), - publicKeyAsHexString(alicePublicKey), - ), - )( - "TransferCommand is created", - _ => { - aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(TransferCommand.COMPANION)( - aliceParty, - c => c.data.sender == aliceParty.toProtoPrimitive, - ) - .loneElement - .id - }, - ) - actAndCheck( - "Resume validator automation for TransferCommands", - aliceValidatorBackend.validatorAutomation.trigger[TransferCommandSendTrigger].resume(), - )( - "TransferCommand gets archived", - _ => { - aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs - .filterJava(TransferCommand.COMPANION)( - aliceParty, - c => c.data.sender == aliceParty.toProtoPrimitive, - ) shouldBe empty withClue "alice TransferCommands" - val result = aliceValidatorBackend.scanProxy - .lookupTransferCommandStatus( - aliceParty, - 2L, - ) - .value - result.transferCommandsByContractId.values.loneElement.status shouldBe definitions.TransferCommandContractStatus.members - .TransferCommandFailedResponse( - definitions.TransferCommandFailedResponse( - status = "failed", - failureKind = - definitions.TransferCommandFailedResponse.FailureKind.members.Failed, - reason = - s"ITR_Other(No TransferPreapproval for receiver '${sv1Party.toProtoPrimitive}')", - ) - ) - result.transferCommandsByContractId.keys.loneElement shouldBe suffixedCid.contractId - }, - ) - } } "TransferPreapprovals get renewed by validator automation" in { implicit env => diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala index cddcb116a58..0e0082fcecd 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala @@ -1,19 +1,15 @@ package org.lfdecentralizedtrust.splice.integration.tests import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition -import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ - IntegrationTestWithIsolatedEnvironment, - SpliceTestConsoleEnvironment, -} +import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTestWithIsolatedEnvironment import com.digitalasset.canton.{HasExecutionContext, HasTempDirectory} -import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import java.util.UUID import scala.collection.mutable import scala.sys.process.{Process, ProcessLogger} -trait ExternallySignedTxTest +final class ExternallySignedPartyOnboardingTest extends IntegrationTestWithIsolatedEnvironment with HasExecutionContext with ExternallySignedPartyTestUtil @@ -21,11 +17,7 @@ trait ExternallySignedTxTest override def environmentDefinition: SpliceEnvironmentDefinition = { EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName) - }.withTransferCommandSupport - - def prepareAndSubmitTransfer(keyName: String, sender: PartyId, receiver: PartyId)(implicit - env: SpliceTestConsoleEnvironment - ): Unit + } "a ccsp provider" should { @@ -112,67 +104,6 @@ trait ExternallySignedTxTest .getExternalPartyBalance(partyId) .totalUnlockedCoin shouldBe "24.0000000000" } - val partyHint2 = UUID.randomUUID().toString - val keyName2 = "party-key-2" - runProcess( - Seq( - "python", - "scripts/external-signing/external-signing.py", - "generate-key-pair", - s"--key-directory=${tempDirectory.path}", - s"--key-name=$keyName2", - ), - aliceValidatorBackend.token.value, - ) - runProcess( - Seq( - "python", - "scripts/external-signing/external-signing.py", - "setup-party", - s"--validator-url=http://localhost:${aliceValidatorBackend.config.adminApi.port}", - s"--key-directory=${tempDirectory.path}", - s"--key-name=$keyName2", - s"--party-hint=$partyHint2", - ), - aliceValidatorBackend.token.value, - ) - - val partyId2 = aliceValidatorBackend.participantClient.parties - .hosted(filterParty = partyHint2) - .loneElement - .party - - runProcess( - Seq( - "python", - "scripts/external-signing/external-signing.py", - "setup-transfer-preapproval", - s"--validator-url=http://localhost:${aliceValidatorBackend.config.adminApi.port}", - s"--key-directory=${tempDirectory.path}", - s"--key-name=$keyName2", - s"--party-id=${partyId2.toProtoPrimitive}", - ), - aliceValidatorBackend.token.value, - ) - - eventually() { - sv1ScanBackend - .lookupTransferPreapprovalByParty(partyId2) - .value - .payload - .receiver shouldBe partyId2.toProtoPrimitive - } - - actAndCheck( - "Prepare and submit transaction to create TransferCommand", - prepareAndSubmitTransfer(keyName, partyId, partyId2), - )( - "DSO automation completes transfer", - _ => - aliceValidatorBackend - .getExternalPartyBalance(partyId2) - .totalUnlockedCoin shouldBe "20.0000000000", - ) } } @@ -188,26 +119,3 @@ trait ExternallySignedTxTest } } } - -class ExternallySignedPartyOnboardingTest extends ExternallySignedTxTest { - override def prepareAndSubmitTransfer(keyName: String, sender: PartyId, receiver: PartyId)( - implicit env: SpliceTestConsoleEnvironment - ) = { - runProcess( - Seq( - "python", - "scripts/external-signing/external-signing.py", - "transfer-preapproval-send", - s"--validator-url=http://localhost:${aliceValidatorBackend.config.adminApi.port}", - s"--scan-url=http://localhost:${sv1ScanBackend.config.adminApi.port}", - s"--key-directory=${tempDirectory.path}", - s"--key-name=$keyName", - s"--sender-party-id=${sender.toProtoPrimitive}", - s"--receiver-party-id=${receiver.toProtoPrimitive}", - s"--amount=20.0", - s"--nonce=0", - ), - aliceValidatorBackend.token.value, - ) - } -} diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala index afda9edb615..8e98d41a46c 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala @@ -1,9 +1,7 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.daml.ledger.api.v2.interactive.InteractiveSubmissionServiceOuterClass.PreparedTransaction -import com.digitalasset.canton.crypto.{SigningKeyUsage, SigningPrivateKey} import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.util.HexString import org.lfdecentralizedtrust.splice.http.v0.definitions import org.lfdecentralizedtrust.splice.http.v0.definitions.DamlValueEncoding.members.CompactJson import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition @@ -26,7 +24,6 @@ class ExternallySignedTxsTimeBasedIntegrationTest override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) - .withTransferCommandSupport "Externally signed transactions can tolerate a preparation/submission skew larger than ledgerTimeRecordTimeTolerance" in { implicit env => @@ -84,80 +81,6 @@ class ExternallySignedTxsTimeBasedIntegrationTest val preparePartySetup2 = prepareAcceptExternalPartySetupProposal(aliceValidatorBackend, onboarding2, proposal2) submitExternalPartySetupProposal(aliceValidatorBackend, onboarding2, preparePartySetup2) - - // Transfer 10 amulets from ext party 1 to ext party 2 - val prepareSend = - aliceValidatorBackend.prepareTransferPreapprovalSend( - onboarding1.party, - onboarding2.party, - BigDecimal(10.0), - CantonTimestamp.now().plus(Duration.ofHours(24)), - 0L, - Some("transfer-command-description"), - ) - val preparedTxSend = - PreparedTransaction.parseFrom(Base64.getDecoder.decode(prepareSend.transaction)) - - // Advance time by a little less than the preparationTimeRecordTimeTolerance of 24h - advanceTime(Duration.ofHours(23)) - actAndCheck( - "Submit signed TransferCommand creation", - aliceValidatorBackend.submitTransferPreapprovalSend( - onboarding1.party, - prepareSend.transaction, - HexString.toHexString( - crypto(env.executionContext) - .signBytes( - HexString.parseToByteString(prepareSend.txHash).value, - onboarding1.privateKey.asInstanceOf[SigningPrivateKey], - usage = SigningKeyUsage.ProtocolOnly, - ) - .value - .toProtoV30 - .signature - ), - publicKeyAsHexString(onboarding1.publicKey), - ), - )( - "Validator automation completes transfer", - updateId => { - val result = aliceValidatorBackend.scanProxy - .lookupTransferCommandStatus( - onboarding1.party, - 0L, - ) - .value - result.transferCommandsByContractId should have size 1 withClue "transferCommandsByContractId" - val transferCommandCid = result.transferCommandsByContractId.keys - .find(_.startsWith(prepareSend.transferCommandContractIdPrefix)) - .value - result.transferCommandsByContractId - .get(transferCommandCid) - .value - .status shouldBe definitions.TransferCommandContractStatus.members - .TransferCommandSentResponse( - definitions.TransferCommandSentResponse(status = "sent") - ) - aliceValidatorBackend - .getExternalPartyBalance(onboarding2.party) - .totalUnlockedCoin shouldBe "10.0000000000" - val update = eventuallySucceeds() { - sv1ScanBackend.getUpdate(updateId, encoding = CompactJson) - } - inside(update) { - case definitions.UpdateHistoryItem.members.UpdateHistoryTransaction(transaction) => - val submissionTime = - CantonTimestamp.ofEpochMicro(preparedTxSend.getMetadata.getPreparationTime) - val actualRecordTime = - CantonTimestamp.assertFromInstant(java.time.Instant.parse(transaction.recordTime)) - val expectedRecordTime = submissionTime.plus(Duration.ofHours(23)) - // We allow for a 1s buffer since time is not completely still in simtime, the microseconds still advance. - actualRecordTime should be >= expectedRecordTime - actualRecordTime should be < expectedRecordTime.plus(Duration.ofSeconds(1)) - } - }, - ) - val beforePrepare = env.environment.clock.now executeTransferViaTokenStandard( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppActivityMarkerIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppActivityMarkerIntegrationTest.scala index 8d96cd2797d..2942c295f19 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppActivityMarkerIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppActivityMarkerIntegrationTest.scala @@ -32,6 +32,8 @@ class FeaturedAppActivityMarkerIntegrationTest EnvironmentDefinition // Using 4Svs so that we see whether they manage to jointly complete all work .simpleTopology4Svs(this.getClass.getSimpleName) + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) .addConfigTransforms((_, config) => ConfigTransforms.updateAllSvAppFoundDsoConfigs_( _.copy( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppRightSwitchOverTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppRightSwitchOverTimeBasedIntegrationTest.scala new file mode 100644 index 00000000000..3b15247b266 --- /dev/null +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FeaturedAppRightSwitchOverTimeBasedIntegrationTest.scala @@ -0,0 +1,171 @@ +package org.lfdecentralizedtrust.splice.integration.tests + +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.topology.PartyId +import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.{ + holdingv1, + metadatav1, + transferinstructionv1, +} +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ + DsoRulesConfig, + DsoRules_SetConfig, +} +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.actionrequiringconfirmation.ARC_DsoRules +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.dsorules_actionrequiringconfirmation.SRARC_SetConfig +import org.lfdecentralizedtrust.splice.config.ConfigTransforms +import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition +import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ + IntegrationTest, + SpliceTestConsoleEnvironment, +} +import org.lfdecentralizedtrust.splice.util.{ + ChoiceContextWithDisclosures, + SwitchOverTimes, + TimeTestUtil, + WalletTestUtil, +} + +import java.time.Duration +import java.util.Optional +import scala.jdk.CollectionConverters.* +import scala.jdk.OptionConverters.* + +@org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceDsoGovernance_0_1_29 +class FeaturedAppRightSwitchOverTimeBasedIntegrationTest + extends IntegrationTest + with TimeTestUtil + with WalletTestUtil { + + override def environmentDefinition: EnvironmentDefinition = + EnvironmentDefinition + .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) + .addConfigTransform((_, config) => + ConfigTransforms.updateAllSvAppFoundDsoConfigs_( + _.copy(initialSvOperationsSwitchOverTimes = None) + )(config) + ) + + "scan stops serving the featured app right in transfer contexts at the switch-over time" in { + implicit env => + val aliceUserParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) + val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId() + + clue("The DSO bootstraps without any switch-over times") { + sv1ScanBackend + .getDsoInfo() + .dsoRules + .payload + .config + .svOperationsSwitchOverTimes + .toScala + .map(_.asScala.toMap) shouldBe None + } + + aliceValidatorWalletClient.tap(50.0) + createTransferPreapprovalEnsuringItExists(aliceValidatorWalletClient, aliceValidatorBackend) + grantFeaturedAppRight(aliceValidatorWalletClient) + val featuredAppRight = sv1ScanBackend.lookupFeaturedAppRight(aliceValidatorParty).value + + clue("Without a switch-over time the transfer context contains the featured app right") { + transferContext(aliceUserParty, aliceValidatorParty).values.asScala + .get("featured-app-right") + .value shouldBe new metadatav1.anyvalue.AV_ContractId( + new metadatav1.AnyContract.ContractId(featuredAppRight.contractId.contractId) + ) + } + + val switchOverTime = CantonTimestamp.assertFromInstant( + getLedgerTime.toInstant.plus(Duration.ofMinutes(10)) + ) + actAndCheck( + "Vote in a switch-over time in the future", + setNoFeaturedAppChoiceContextSwitchOverTime(switchOverTime), + )( + "Scan observes the new DsoRules config", + _ => + sv1ScanBackend + .getDsoInfo() + .dsoRules + .payload + .config + .svOperationsSwitchOverTimes + .toScala + .map(_.asScala.toMap) shouldBe Some( + Map(SwitchOverTimes.NoFeaturedAppChoiceContext -> switchOverTime.toInstant) + ), + ) + + clue("Before the switch-over time the featured app right is still included") { + transferContext(aliceUserParty, aliceValidatorParty).values.asScala + .get("featured-app-right") + .value shouldBe new metadatav1.anyvalue.AV_ContractId( + new metadatav1.AnyContract.ContractId(featuredAppRight.contractId.contractId) + ) + } + + advanceTime(Duration.ofMinutes(11)) + + clue("After the switch-over time the featured app right is no longer included") { + transferContext( + aliceUserParty, + aliceValidatorParty, + ).values.asScala.keySet shouldNot contain("featured-app-right") + } + } + + private def transferContext(sender: PartyId, receiver: PartyId)(implicit + env: SpliceTestConsoleEnvironment + ): metadatav1.ChoiceContext = { + val now = getLedgerTime.toInstant + val choiceArgs = new transferinstructionv1.TransferFactory_Transfer( + dsoParty.toProtoPrimitive, + new transferinstructionv1.Transfer( + sender.toProtoPrimitive, + receiver.toProtoPrimitive, + BigDecimal(10).bigDecimal, + new holdingv1.InstrumentId(dsoParty.toProtoPrimitive, "Amulet"), + now, + now.plus(Duration.ofMinutes(10)), + // The holdings are irrelevant for the choice context served by scan. + Seq.empty[holdingv1.Holding.ContractId].asJava, + ChoiceContextWithDisclosures.emptyMetadata, + ), + ChoiceContextWithDisclosures.emptyExtraArgs, + ) + val (factory, _) = sv1ScanBackend.getTransferFactory(choiceArgs) + factory.args.extraArgs.context + } + + private def setNoFeaturedAppChoiceContextSwitchOverTime( + switchOverTime: CantonTimestamp + )(implicit env: SpliceTestConsoleEnvironment): Unit = { + val config = sv1Backend.getDsoInfo().dsoRules.payload.config + val newConfig = new DsoRulesConfig( + config.numUnclaimedRewardsThreshold, + config.numMemberTrafficContractsThreshold, + config.actionConfirmationTimeout, + config.svOnboardingRequestTimeout, + config.svOnboardingConfirmedTimeout, + config.voteRequestTimeout, + config.dsoDelegateInactiveTimeout, + config.synchronizerNodeConfigLimits, + config.maxTextLength, + config.decentralizedSynchronizer, + config.nextScheduledSynchronizerUpgrade, + config.voteCooldownTime, + config.nextScheduledLogicalSynchronizerUpgrade, + Optional.of( + Map(SwitchOverTimes.NoFeaturedAppChoiceContext -> switchOverTime.toInstant).asJava + ), + ) + sv1Backend.createVoteRequest( + sv1Backend.getDsoInfo().svParty.toProtoPrimitive, + new ARC_DsoRules(new SRARC_SetConfig(new DsoRules_SetConfig(newConfig, Optional.empty()))), + "url", + "set the no-featured-app-choice-context switch-over time", + config.voteRequestTimeout, + None, + ) + } +} diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FrontendIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FrontendIntegrationTest.scala index bd7411d1c10..632f0314b5e 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FrontendIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FrontendIntegrationTest.scala @@ -775,6 +775,7 @@ trait FrontendTestCommon extends TestCommon with WebBrowser with CustomMatchers clue(s"$party selects the date $dateTime") { val dateTimePicker = webDriver.findElement(By.id(pickerId)) eventually() { + webDriver.executeScript("arguments[0].scrollIntoView({block: 'center'});", dateTimePicker) dateTimePicker.clear() dateTimePicker.click() // Typing in the "filler" characters can mess up the input badly diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala index 59e3baa1f1c..54b07b462ce 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala @@ -15,7 +15,6 @@ import com.digitalasset.canton.logging.SuppressionRule import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId.Synchronizer import com.digitalasset.canton.topology.store.TimeQuery import com.digitalasset.canton.topology.transaction.TopologyChangeOp -import com.digitalasset.canton.util.HexString import com.digitalasset.canton.version.ProtocolVersion import monocle.macros.syntax.lens.* import org.lfdecentralizedtrust.splice.config.{ @@ -52,6 +51,7 @@ import scala.collection.parallel.CollectionConverters.seqIsParallelizable import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.MapHasAsScala import scala.jdk.OptionConverters.RichOptional +import org.lfdecentralizedtrust.tokenstandard.transferinstruction @org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceDsoGovernance_0_1_24 class LsuIntegrationTest @@ -64,7 +64,8 @@ class LsuIntegrationTest with HasExecutionContext with SynchronizerFeesTestUtil with LsuTestUtil - with TryValues { + with TryValues + with TokenStandardTest { override protected def runEventHistorySanityCheck: Boolean = false override protected lazy val resetRequiredTopologyState: Boolean = false @@ -219,7 +220,6 @@ class LsuIntegrationTest .withSvBftSequencerConnectionDisabled() .withAmuletPrice(walletAmuletPrice) .withManualStart - .withTransferCommandSupport override def walletAmuletPrice: java.math.BigDecimal = SpliceUtil.damlDecimal(1.0) @@ -705,42 +705,24 @@ class LsuIntegrationTest aliceValidatorBackend .getExternalPartyBalance(externalPartyOnboarding.party) .totalUnlockedCoin shouldBe "40.0000000000" - val prepareSend = - aliceValidatorBackend.prepareTransferPreapprovalSend( - externalPartyOnboarding.party, + + actAndCheck( + "external party transfer 10 cc", + executeTransferViaTokenStandard( + aliceValidatorBackend.participantClientWithAdminToken, + externalPartyOnboarding.richPartyId, aliceValidatorBackend.getValidatorPartyId(), - BigDecimal(10.0), - CantonTimestamp.now().plus(Duration.ofHours(24)), - 0L, - Some("transfer-command-description"), - ) - actAndCheck(timeUntilSuccess = 1.minute)( - "Submit signed TransferCommand creation", - aliceValidatorBackend.submitTransferPreapprovalSend( - externalPartyOnboarding.party, - prepareSend.transaction, - HexString.toHexString( - crypto - .signBytes( - HexString.parseToByteString(prepareSend.txHash).value, - externalPartyOnboarding.privateKey.asInstanceOf[SigningPrivateKey], - usage = SigningKeyUsage.ProtocolOnly, - ) - .value - .toProtoV30 - .signature - ), - publicKeyAsHexString(externalPartyOnboarding.publicKey), + BigDecimal("10.0"), + transferinstruction.v1.definitions.TransferFactoryWithChoiceContext.TransferKind.Direct, ), )( - "validator automation completes transfer", - _ => { + "external party's balance decreases by 10 cc", + _ => BigDecimal( aliceValidatorBackend .getExternalPartyBalance(externalPartyOnboarding.party) .totalUnlockedCoin - ) should be(BigDecimal(30)) - }, + ) should be(BigDecimal(30)), ) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala index fbacabfd1ab..7b284cc8bde 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala @@ -13,15 +13,11 @@ import com.daml.nonempty.NonEmpty import com.digitalasset.canton.admin.api.client.commands.TopologyAdminCommands.Write.GenerateTransactions import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.crypto.* -import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId import com.digitalasset.canton.topology.transaction.* -import com.digitalasset.canton.util.HexString import org.lfdecentralizedtrust.splice.util.WalletTestUtil import java.nio.file.Files -import java.time.Duration -import scala.concurrent.duration.* @org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceAmulet_0_1_9 class RecoverExternalPartyIntegrationTest @@ -31,7 +27,7 @@ class RecoverExternalPartyIntegrationTest with WalletTestUtil { override def environmentDefinition: EnvironmentDefinition = - EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName).withTransferCommandSupport + EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName) override protected lazy val sanityChecksIgnoredRootCreates = Seq( ValidatorRewardCoupon.TEMPLATE_ID_WITH_PACKAGE_ID @@ -208,43 +204,23 @@ class RecoverExternalPartyIntegrationTest } // Check that alice can transfer through bob's node - val prepareSend = - bobValidatorBackend.prepareTransferPreapprovalSend( - aliceParty, + actAndCheck( + "alice transfer 1000 cc to bob's validator", + executeTransferViaTokenStandard( + bobValidatorBackend.participantClientWithAdminToken, + onboarding.richPartyId, bobValidatorBackend.getValidatorUserInfo().primaryParty, BigDecimal(1000.0), - CantonTimestamp.now().plus(Duration.ofHours(24)), - 0L, - Some("transfer-command-description"), - verboseHashing = true, - ) - val (updateId, _) = actAndCheck(timeUntilSuccess = 60.seconds)( - "Submit signed TransferCommand creation", - bobValidatorBackend.submitTransferPreapprovalSend( - aliceParty, - prepareSend.transaction, - HexString.toHexString( - crypto(env.executionContext) - .signBytes( - HexString.parseToByteString(prepareSend.txHash).value, - alicePrivateKey.asInstanceOf[SigningPrivateKey], - usage = SigningKeyUsage.ProtocolOnly, - ) - .value - .toProtoV30 - .signature - ), - publicKeyAsHexString(alicePublicKey), + transferinstruction.v1.definitions.TransferFactoryWithChoiceContext.TransferKind.Direct, ), )( - "validator automation completes transfer", - _ => { + "alice balance decreases", + _ => BigDecimal( bobValidatorBackend .getExternalPartyBalance(aliceParty) .totalUnlockedCoin - ) should be(BigDecimal(1000)) - }, + ) should be(BigDecimal(1000)), ) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala index 04493aa242e..71fe3e4114b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala @@ -13,6 +13,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.AmuletRul import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.svstate.SvNodeState import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.DsoRules import org.lfdecentralizedtrust.splice.config.ConfigTransforms +import org.lfdecentralizedtrust.splice.config.PerClientIpRateLimitConfig import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ updateAutomationConfig, ConfigurableApp, @@ -75,10 +76,9 @@ class ScanIntegrationTest // used for the rate limit test rateLimiting = config.parameters.rateLimiting.copy( rateLimiters = - config.parameters.rateLimiting.rateLimiters + ("listAnsEntries" -> SpliceRateLimitConfig - .WithPerClientIp( - ratePerSecond = 5 - )) + config.parameters.rateLimiting.rateLimiters + ("listAnsEntries" -> PerClientIpRateLimitConfig( + ratePerSecond = 5 + )) ), ), ) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellIntegrationTest.scala index 9f18a8cec97..2e78d9c20a8 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SplitwellIntegrationTest.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.integration.tests import com.digitalasset.canton.SynchronizerAlias import org.lfdecentralizedtrust.splice.codegen.java.splice.splitwell as splitwellCodegen import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.payment as walletCodegen +import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest import org.lfdecentralizedtrust.splice.splitwell.automation.AcceptedAppPaymentRequestsTrigger @@ -20,6 +21,8 @@ class SplitwellIntegrationTest override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .simpleTopology1Sv(this.getClass.getSimpleName) + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) .withAdditionalSetup(implicit env => { aliceValidatorBackend.participantClient.upload_dar_unless_exists(splitwellDarPath) bobValidatorBackend.participantClient.upload_dar_unless_exists(splitwellDarPath) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvExpiredRewardsCollectionTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvExpiredRewardsCollectionTimeBasedIntegrationTest.scala index 5c741a08418..d1e9b56e38d 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvExpiredRewardsCollectionTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvExpiredRewardsCollectionTimeBasedIntegrationTest.scala @@ -18,13 +18,16 @@ class SvExpiredRewardsCollectionTimeBasedIntegrationTest with SvTestUtil { override def environmentDefinition = - super.environmentDefinition.addConfigTransform((_, config) => - ConfigTransforms.updateAllValidatorConfigs_( - // Bump lifetime above base duration to burn fees and generate validator rewards - _.focus(_.transferPreapproval.preapprovalLifetime) - .replace(NonNegativeFiniteDuration.ofDays(100)) - )(config) - ) + super.environmentDefinition + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) + .addConfigTransform((_, config) => + ConfigTransforms.updateAllValidatorConfigs_( + // Bump lifetime above base duration to burn fees and generate validator rewards + _.focus(_.transferPreapproval.preapprovalLifetime) + .replace(NonNegativeFiniteDuration.ofDays(100)) + )(config) + ) "collect expired reward coupons" in { implicit env => def getRewardCoupons( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala index 7475082bbfa..aef8a27e22a 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala @@ -145,6 +145,9 @@ class SvReconcileSynchronizerConfigIntegrationTest extends SvIntegrationTestBase amuletConfig.externalPartyConfigStateTickDuration, amuletConfig.rewardConfig, amuletConfig.transferPreapprovalBaseDuration, + amuletConfig.developmentFundManagerBlacklist, + amuletConfig.minDevelopmentFundMintingDelay, + amuletConfig.amuletSwitchOverTimes, ) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala index 1dd895639dc..4b5f6941343 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala @@ -397,6 +397,7 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge sv1Backend.getDsoInfo().dsoRules.payload.config.nextScheduledSynchronizerUpgrade, sv1Backend.getDsoInfo().dsoRules.payload.config.voteCooldownTime, sv1Backend.getDsoInfo().dsoRules.payload.config.nextScheduledLogicalSynchronizerUpgrade, + sv1Backend.getDsoInfo().dsoRules.payload.config.svOperationsSwitchOverTimes, ) val action: ActionRequiringConfirmation = @@ -506,6 +507,9 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge initialConfig.externalPartyConfigStateTickDuration, initialConfig.rewardConfig, initialConfig.transferPreapprovalBaseDuration, + initialConfig.developmentFundManagerBlacklist, + initialConfig.minDevelopmentFundMintingDelay, + initialConfig.amuletSwitchOverTimes, ) val (_, voteRequestCid) = actAndCheck( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala index 28dc924e858..a74e8c6169d 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedOnboardingIntegrationTest.scala @@ -205,6 +205,12 @@ class SvTimeBasedOnboardingIntegrationTest .payload .config .nextScheduledLogicalSynchronizerUpgrade, + sv1Backend + .getDsoInfo() + .dsoRules + .payload + .config + .svOperationsSwitchOverTimes, ) val action: ActionRequiringConfirmation = diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardAllocationIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardAllocationIntegrationTest.scala index 39ba4757f5c..9867639c682 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardAllocationIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardAllocationIntegrationTest.scala @@ -1,7 +1,6 @@ package org.lfdecentralizedtrust.splice.integration.tests -import com.digitalasset.daml.lf.data.Ref.PackageVersion -import com.daml.ledger.javaapi.data.CreatedEvent +import com.daml.ledger.javaapi.data.{CreatedEvent, ExercisedEvent} import com.digitalasset.canton.admin.api.client.data.TemplateId import com.digitalasset.canton.HasExecutionContext import com.digitalasset.canton.topology.PartyId @@ -11,7 +10,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.{ allocationv1, metadatav1, } -import org.lfdecentralizedtrust.splice.environment.DarResources +import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ IntegrationTestWithIsolatedEnvironment, @@ -19,7 +18,6 @@ import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ } import org.lfdecentralizedtrust.splice.util.{ ChoiceContextWithDisclosures, - JavaDecodeUtil, TriggerTestUtil, WalletTestUtil, } @@ -27,10 +25,7 @@ import org.lfdecentralizedtrust.splice.util.{ import scala.jdk.CollectionConverters.* import scala.util.Random import com.digitalasset.canton.util.ShowUtil.* -import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.{ - AppRewardCoupon, - FeaturedAppActivityMarker, -} +import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.{FeaturedAppActivityMarker} import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletallocation as amuletallocationCodegen import org.lfdecentralizedtrust.splice.console.WalletAppClientReference import org.lfdecentralizedtrust.splice.integration.tests.TokenStandardTest.CreateAllocationRequestResult @@ -38,6 +33,7 @@ import org.lfdecentralizedtrust.splice.util.PrettyInstances.* import org.lfdecentralizedtrust.splice.wallet.admin.api.client.commands.HttpWalletAppClient @org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceTokenTestTradingApp_1_0_0 +@org.lfdecentralizedtrust.splice.util.scalatesttags.SpliceDsoGovernance_0_1_29 class TokenStandardAllocationIntegrationTest extends IntegrationTestWithIsolatedEnvironment with HasExecutionContext @@ -50,6 +46,8 @@ class TokenStandardAllocationIntegrationTest override def environmentDefinition: EnvironmentDefinition = { EnvironmentDefinition .simpleTopology1Sv(this.getClass.getSimpleName) + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) .withAdditionalSetup(implicit env => { Seq( sv1ValidatorBackend, @@ -181,30 +179,14 @@ class TokenStandardAllocationIntegrationTest } } val events = tree.getEventsById().asScala.values - forExactly(1, events) { - inside(_) { case c: CreatedEvent => - if ( - PackageVersion.assertFromString( - sv1ScanBackend - .getAmuletRules() - .payload - .configSchedule - .initialValue - .packageConfig - .amulet - ) >= DarResources.amulet_0_1_17.metadata.version - ) { - val decoded = JavaDecodeUtil - .decodeCreated(FeaturedAppActivityMarker.COMPANION)(c) - .value - decoded.data.provider shouldBe allocatedOtcTrade.venueParty.toProtoPrimitive - } else { - val decoded = JavaDecodeUtil - .decodeCreated(AppRewardCoupon.COMPANION)(c) - .value - decoded.data.featured shouldBe true - decoded.data.provider shouldBe allocatedOtcTrade.venueParty.toProtoPrimitive - } + forAll(events) { + inside(_) { + case c: CreatedEvent => + Seq( + FeaturedAppActivityMarker.TEMPLATE_ID, + FeaturedAppActivityMarker.TEMPLATE_ID, + ) shouldNot contain(c.getTemplateId) + case _: ExercisedEvent => succeed } } }, diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala index 036c3d7e390..dd1687e584e 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TokenStandardCliTestDataTimeBasedIntegrationTest.scala @@ -51,7 +51,7 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{ updateAllScanAppConfigs_, updateAutomationConfig, } -import org.lfdecentralizedtrust.splice.config.RateLimitersConfig +import org.lfdecentralizedtrust.splice.config.{PerClientIpRateLimitConfig, RateLimitersConfig} import org.lfdecentralizedtrust.splice.console.LedgerApiExtensions.RichPartyId import org.lfdecentralizedtrust.splice.http.v0.definitions.TransferInstructionResultOutput.members import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition @@ -65,12 +65,7 @@ import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ ExpiredAmuletAllocationV2Trigger, ExpiredAmuletTransferInstructionTrigger, } -import org.lfdecentralizedtrust.splice.util.{ - SpliceRateLimitConfig, - TimeTestUtil, - TriggerTestUtil, - WalletTestUtil, -} +import org.lfdecentralizedtrust.splice.util.{TimeTestUtil, TriggerTestUtil, WalletTestUtil} import org.lfdecentralizedtrust.splice.wallet.admin.api.client.commands.HttpWalletAppClient import org.lfdecentralizedtrust.splice.wallet.automation.CollectRewardsAndMergeAmuletsTrigger import org.lfdecentralizedtrust.tokenstandard.transferinstruction @@ -145,9 +140,9 @@ class TokenStandardCliTestDataTimeBasedIntegrationTest config.copy(parameters = config.parameters.copy(rateLimiting = RateLimitersConfig( - default = SpliceRateLimitConfig.WithPerClientIp(enabled = false, 1), + default = PerClientIpRateLimitConfig(enabled = false, 1), rateLimiters = Map.empty, - global = SpliceRateLimitConfig.WithPerClientIp(enabled = false, 1), + global = PerClientIpRateLimitConfig(enabled = false, 1), ) ) ) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala index 13a38c0fd04..2242956f24f 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/TrafficBasedRewardsSvAppTimeBasedIntegrationTest.scala @@ -822,6 +822,9 @@ class TrafficBasedRewardsSvAppTimeBasedIntegrationTest existing.externalPartyConfigStateTickDuration, Optional.of(newRc), existing.transferPreapprovalBaseDuration, + existing.developmentFundManagerBlacklist, + existing.minDevelopmentFundMintingDelay, + existing.amuletSwitchOverTimes, ) setAmuletConfig(Seq((None, newConfig, existing))) eventually() { diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala index 5f3f8d097a3..77c6a165f9d 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest.scala @@ -32,7 +32,6 @@ import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.{ ExpireRewardCouponV2Trigger, UnhideRewardCouponV2Trigger, } -import org.lfdecentralizedtrust.splice.sv.config.InitialRewardConfig import org.lfdecentralizedtrust.splice.util.{ ChoiceContextWithDisclosures, TimeTestUtil, @@ -104,29 +103,23 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest EnvironmentDefinition .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) .withNoVettedPackages(implicit env => Seq(aliceValidatorBackend.participantClient)) - .addConfigTransforms((_, config) => { - val aliceValidator = InstanceName.tryCreate("aliceValidator") - config.copy( - validatorApps = config.validatorApps + - (aliceValidator -> config - .validatorApps(aliceValidator) - .copy( - additionalPackagesToUnvet = darsUnvettedOnAliceAtStart - .groupBy(_.metadata.name) - .map { case (name, resources) => - name -> resources.map(_.metadata.version).toSet - } - )) - ) - }) - .addConfigTransform((_, config) => - ConfigTransforms.withRewardConfig( - InitialRewardConfig( - mintingVersion = "RewardVersion_TrafficBasedAppRewards", - dryRunVersion = None, - appRewardCouponThreshold = BigDecimal("0"), + .addConfigTransforms( + (_, config) => { + val aliceValidator = InstanceName.tryCreate("aliceValidator") + config.copy( + validatorApps = config.validatorApps + + (aliceValidator -> config + .validatorApps(aliceValidator) + .copy( + additionalPackagesToUnvet = darsUnvettedOnAliceAtStart + .groupBy(_.metadata.name) + .map { case (name, resources) => + name -> resources.map(_.metadata.version).toSet + } + )) ) - )(config) + }, + (_, c) => ConfigTransforms.withNoSvOperationsSwitchOverTimes(c), ) .addConfigTransform((_, config) => updateAutomationConfig(ConfigurableApp.Validator)( @@ -469,6 +462,7 @@ class UnhideAndExpireRewardCouponV2TimeBasedIntegrationTest }, ) } + } private def vettedPackagesOnSv1View( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala index d7f9b156ffd..14f5fb58e75 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala @@ -155,14 +155,16 @@ class UnsupportedPackageVettingIntegrationTest val synchronizerId = sv1Backend.participantClient.synchronizers.list_connected().head.synchronizerId + // Downgrade targets bumped to >= 0.1.19 for TBAR default: versions below + // 0.1.19 lack the rewardConfig field and fail upgrade translation. val validatorDarsAbovePackageConfigVersion = Seq( - DarResources.wallet_0_1_18, - DarResources.walletPayments_0_1_17, - DarResources.amuletNameService_0_1_18, - DarResources.amulet_0_1_17, + DarResources.wallet_0_1_21, + DarResources.walletPayments_0_1_20, + DarResources.amuletNameService_0_1_21, + DarResources.amulet_0_1_20, ) val svDarsAbovePackageConfigVersion = Seq( - DarResources.dsoGovernance_0_1_23 + DarResources.dsoGovernance_0_1_26 ) ++ validatorDarsAbovePackageConfigVersion clue("sv1 votes to downgrade to the previous package versions") { @@ -171,12 +173,12 @@ class UnsupportedPackageVettingIntegrationTest AmuletConfigSchedule(amuletRules).getConfigAsOf(env.environment.clock.now) val downgradedPackageConfig = new PackageConfig( - DarResources.amulet_0_1_16.metadata.version.toString(), - DarResources.amuletNameService_0_1_17.metadata.version.toString(), - DarResources.dsoGovernance_0_1_22.metadata.version.toString(), + DarResources.amulet_0_1_19.metadata.version.toString(), + DarResources.amuletNameService_0_1_20.metadata.version.toString(), + DarResources.dsoGovernance_0_1_25.metadata.version.toString(), currentConfig.packageConfig.validatorLifecycle, - DarResources.wallet_0_1_17.metadata.version.toString(), - DarResources.walletPayments_0_1_16.metadata.version.toString(), + DarResources.wallet_0_1_20.metadata.version.toString(), + DarResources.walletPayments_0_1_19.metadata.version.toString(), ) val newAmuletConfig = new AmuletConfig( currentConfig.transferConfig, @@ -190,6 +192,9 @@ class UnsupportedPackageVettingIntegrationTest currentConfig.externalPartyConfigStateTickDuration, currentConfig.rewardConfig, currentConfig.transferPreapprovalBaseDuration, + currentConfig.developmentFundManagerBlacklist, + currentConfig.minDevelopmentFundMintingDelay, + currentConfig.amuletSwitchOverTimes, ) setAmuletConfig(Seq((None, newAmuletConfig, currentConfig))) } @@ -220,7 +225,7 @@ class UnsupportedPackageVettingIntegrationTest ) should contain noElementsOf validatorDarsAbovePackageConfigVersion.map(_.packageId) } eventually(40.seconds) { - alicesTapsWithPackageId(DarResources.amulet_0_1_16.packageId) + alicesTapsWithPackageId(DarResources.amulet_0_1_19.packageId) } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorFaucetCapZeroTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorFaucetCapZeroTimeBasedIntegrationTest.scala index 7883af91d16..c8f492c4054 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorFaucetCapZeroTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorFaucetCapZeroTimeBasedIntegrationTest.scala @@ -11,7 +11,6 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms import org.lfdecentralizedtrust.splice.environment.BuildInfo import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTestWithIsolatedEnvironment -import org.lfdecentralizedtrust.splice.sv.config.SvOnboardingConfig.InitialPackageConfig import org.lfdecentralizedtrust.splice.util.{DisclosedContracts, TimeTestUtil, WalletTestUtil} import scala.jdk.CollectionConverters.* @@ -24,29 +23,12 @@ class ValidatorFaucetCapZeroTimeBasedIntegrationTest with WalletTestUtil with TimeTestUtil { - // Package versions from Splice 0.5.11 — the Daml versions currently deployed on MainNet, - // which do not include the Daml-side fix for the division-by-zero bug on cap=0 rounds. - // TODO(canton-network/splice#3461): Remove once MainNet has adopted the Daml versions from 0.5.16 or later - private val initialPackageConfig = InitialPackageConfig( - amuletVersion = "0.1.16", - amuletNameServiceVersion = "0.1.17", - dsoGovernanceVersion = "0.1.22", - validatorLifecycleVersion = "0.1.6", - walletVersion = "0.1.17", - walletPaymentsVersion = "0.1.16", - ) - override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) // TODO(canton-network/splice#3461): Remove once MainNet has adopted the Daml versions from 0.5.16 or later .withNoVettedPackages(implicit env => env.validators.local.map(_.participantClient)) .addConfigTransform((_, config) => ConfigTransforms.withValidatorFaucetCap(0)(config)) - .addConfigTransform((_, config) => - ConfigTransforms.updateAllSvAppFoundDsoConfigs_( - _.copy(initialPackageConfig = initialPackageConfig) - )(config) - ) "system works with optValidatorFaucetCap=0 and handles stale liveness records" in { implicit env => diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala index 8ea958b5c68..e343930727b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletMintingDelegationTimeBasedIntegrationTest.scala @@ -470,7 +470,9 @@ class WalletMintingDelegationTimeBasedIntegrationTest val unclaimedActivityAmount = BigDecimal(200.0) val validatorRewardAmount = BigDecimal(500.0) val developmentFundAmount = BigDecimal(300.0) + val delayedDevelopmentFundAmount = BigDecimal(400.0) val rewardCouponV2Amount = BigDecimal(1000.0) + val mintDelay = Duration.ofHours(24) // For ValidatorRewardCoupon, we need ValidatorRight for beneficiary aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands @@ -557,7 +559,7 @@ class WalletMintingDelegationTimeBasedIntegrationTest ).create, ) - // Create DevelopmentFundCoupon + // Create a DevelopmentFundCoupon that is mintable immediately sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands .submitWithResult( userId = sv1Backend.config.ledgerApiUser, @@ -570,6 +572,24 @@ class WalletMintingDelegationTimeBasedIntegrationTest developmentFundAmount.bigDecimal, env.environment.clock.now.plus(Duration.ofDays(1)).toInstant, "test development fund coupon", + java.util.Optional.empty(), // mintAfter + ).create, + ) + + // Create a second DevelopmentFundCoupon with a mintAfter delay + sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands + .submitWithResult( + userId = sv1Backend.config.ledgerApiUser, + actAs = Seq(dsoParty), + readAs = Seq.empty, + update = new DevelopmentFundCoupon( + dsoParty.toProtoPrimitive, + beneficiaryParty.party.toProtoPrimitive, + dsoParty.toProtoPrimitive, // fundManager = dso + delayedDevelopmentFundAmount.bigDecimal, + env.environment.clock.now.plus(Duration.ofDays(30)).toInstant, + "delayed test development fund coupon", + java.util.Optional.of(env.environment.clock.now.plus(mintDelay).toInstant), ).create, ) @@ -587,7 +607,7 @@ class WalletMintingDelegationTimeBasedIntegrationTest val (_, issuingRoundsAfter) = sv1ScanBackend.getOpenAndIssuingMiningRounds() val issuingRoundsMap = issuingRoundsAfter.view.map(r => r.payload.round -> r.payload).toMap - clue("All reward contracts should be consumed") { + clue("All reward contracts except the delayed development fund coupon should be consumed") { eventually() { externalPartyWallet.store .listUnclaimedActivityRecords() @@ -601,9 +621,10 @@ class WalletMintingDelegationTimeBasedIntegrationTest externalPartyWallet.store .listSortedLivenessActivityRecords(issuingRoundsMap) .futureValue shouldBe empty withClue "LivenessActivityRecord" - externalPartyWallet.store - .listDevelopmentFundCoupons() - .futureValue shouldBe empty withClue "DevelopmentFundCoupon" + inside(externalPartyWallet.store.listDevelopmentFundCoupons().futureValue) { + case Seq(remaining) => + remaining.payload.reason shouldBe "delayed test development fund coupon" + } externalPartyWallet.store .listRewardCouponsV2(includeUnassigned = true, includeAssigned = true) .futureValue shouldBe empty withClue "RewardCouponV2" @@ -627,6 +648,20 @@ class WalletMintingDelegationTimeBasedIntegrationTest actualIncrease shouldBe expectedTotalReward + actAndCheck( + "Advance past the delayed coupon's mintAfter", + advanceTime(mintDelay.plus(Duration.ofHours(1))), + )( + "The delayed development fund coupon is collected", + _ => { + advanceTime(Duration.ofSeconds(1)) + externalPartyWallet.store + .listDevelopmentFundCoupons() + .futureValue shouldBe empty withClue "DevelopmentFundCoupon after mintAfter" + getBalance() shouldBe balanceAfter + delayedDevelopmentFundAmount + }, + ) + // Test merge behavior at 2x limit def getAmuletCount() = { externalPartyWallet.store.multiDomainAcsStore diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala index f7b02ba4268..d4a8226272b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala @@ -66,6 +66,8 @@ class WalletTxLogIntegrationTest // The wallet automation periodically merges amulets, which leads to non-deterministic balance changes. // We disable the automation for this suite. .withoutAutomaticRewardsCollectionAndAmuletMerging + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) // Set a non-unit amulet price to better test CC-USD conversion. .addConfigTransform((_, config) => ConfigTransforms.setAmuletPrice(amuletPrice)(config)) .addConfigTransform((_, config) => diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogWithRewardsCollectionTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogWithRewardsCollectionTimeBasedIntegrationTest.scala index c53b46fb260..0225cc1ba5b 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogWithRewardsCollectionTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogWithRewardsCollectionTimeBasedIntegrationTest.scala @@ -31,6 +31,8 @@ class WalletTxLogWithRewardsCollectionTimeBasedIntegrationTest .simpleTopology1SvWithSimTime(this.getClass.getSimpleName) // Set a non-unit amulet price to better test CC-USD conversion. .addConfigTransform((_, config) => ConfigTransforms.setAmuletPrice(amuletPrice)(config)) + // Uses FeaturedAppMarkers: test asserts on AppRewardCoupon which TBAR replaces with RewardCouponV2 + .addConfigTransform((_, config) => ConfigTransforms.withFeaturedAppMarkers(config)) .addConfigTransforms( (_, config) => // without this, you can have 1 or 2 transfers in the txlog, or just 1 with different balance diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RateLimitPreflightIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RateLimitPreflightIntegrationTest.scala index ecd5b80c491..c0d91ce6c49 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RateLimitPreflightIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/runbook/RateLimitPreflightIntegrationTest.scala @@ -13,7 +13,8 @@ import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{ import org.scalatest.Assertion import org.slf4j.event.Level -import scala.concurrent.{Future, blocking} +import scala.concurrent.duration.* +import scala.concurrent.{Await, Future, blocking} import scala.util.control.NonFatal import scala.util.{Failure, Try} @@ -31,7 +32,7 @@ class RateLimitPreflightIntegrationTest extends IntegrationTest { forAll(Table("scan", env.scans.remote*)) { scanCli => val dsoParty = scanCli.getDsoPartyId() rateLimitIsEnforced( - 10, { + 20, { scanCli.getAcsSnapshot( // Dummy party that doesn't exist to avoid creating load PartyId.tryCreate( @@ -66,21 +67,25 @@ class RateLimitPreflightIntegrationTest extends IntegrationTest { ): Assertion = { val results = loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.ERROR))( collectResponses(limit, call), - forAll(_)( - // This hits the Canton limit on concurrent requests - _.message should include( - "Reached the limit of concurrent requests for com.digitalasset.canton.admin.participant.v30.ParticipantRepairService/ExportAcs" - ) + forAll(_)(entry => + forAtLeast( + 1, + Seq( + // This hits the Canton limit on concurrent requests + "Reached the limit of concurrent requests for com.digitalasset.canton.admin.participant.v30.ParticipantRepairService/ExportAcs", + // This hits the app's own rate limiter + "Too Many Requests", + ), + )(entry.message should include(_)) ), ) // Note: failures are expected due to the Canton rate limiter. forAtLeast(1, results) { _ shouldBe a[scala.util.Success[?]] } - // This now hits istio rate limit assertThrowsAndLogsCommandFailures( call, - entry => entry.message should include("HTTP 429 Too Many Requests"), + entry => entry.message should include("Too Many Requests"), ) } @@ -90,23 +95,29 @@ class RateLimitPreflightIntegrationTest extends IntegrationTest { } should be(empty) } - private def collectResponses(limit: Int, call: => Unit)(implicit + private def collectResponses( + limit: Int, + call: => Unit, + timeout: FiniteDuration = 1.minute, + )(implicit env: SpliceTestConsoleEnvironment - ) = { + ): Seq[Try[Unit]] = { import env.executionContext - MonadUtil - .parTraverseWithLimit(PositiveInt.MaxValue)( - Seq.fill(limit)(()) - )(_ => { - Future { - blocking { - Try { - call + Await.result( + MonadUtil + .parTraverseWithLimit(PositiveInt.tryCreate(32))( + Seq.fill(limit)(()) + )(_ => { + Future { + blocking { + Try { + call + } } } - } - }) - .futureValue + }), + timeout, + ) } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala index edbe165ba1c..3f11b93d07d 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala @@ -66,6 +66,9 @@ trait AmuletConfigUtil extends TestCommon { existingAmuletConfig.externalPartyConfigStateTickDuration, existingAmuletConfig.rewardConfig, existingAmuletConfig.transferPreapprovalBaseDuration, + existingAmuletConfig.developmentFundManagerBlacklist, + existingAmuletConfig.minDevelopmentFundMintingDelay, + existingAmuletConfig.amuletSwitchOverTimes, ) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/SvTestUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/SvTestUtil.scala index 7f7bcb1cfba..11b6e946665 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/SvTestUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/SvTestUtil.scala @@ -328,6 +328,7 @@ trait SvTestUtil extends TestCommon { domainUpgradeSchedule.toJava, dsoRulesConfig.voteCooldownTime, dsoRulesConfig.nextScheduledLogicalSynchronizerUpgrade, + dsoRulesConfig.svOperationsSwitchOverTimes, ) private def updateNextScheduledLogicalSynchronizerUpgrade( @@ -347,6 +348,7 @@ trait SvTestUtil extends TestCommon { dsoRulesConfig.nextScheduledSynchronizerUpgrade, dsoRulesConfig.voteCooldownTime, schedule.toJava, + dsoRulesConfig.svOperationsSwitchOverTimes, ) def computeAmuletsToIssueToSvs( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala index 1422e68567d..94175430e84 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/WalletTestUtil.scala @@ -1133,6 +1133,7 @@ trait WalletTestUtil extends TestCommon with AnsTestUtil { amount.bigDecimal, expiresAt.toInstant, reason, + java.util.Optional.empty(), // mintAfter ).create val created = participantClient.ledger_api_extensions.commands .submitWithResult( diff --git a/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts b/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts index 5a0ac947cc9..aeb0b364052 100644 --- a/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts +++ b/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts @@ -82,6 +82,7 @@ export function getAmuletRulesConfig( baseRateTrafficLimitsBurstWindow: string = '1200000000' ): AmuletConfig { return { + amuletSwitchOverTimes: null, packageConfig: { amuletNameService: '0.1.8', walletPayments: '0.1.8', @@ -230,6 +231,8 @@ export function getAmuletRulesConfig( externalPartyConfigStateTickDuration: null, rewardConfig: null, transferPreapprovalBaseDuration: null, + developmentFundManagerBlacklist: null, + minDevelopmentFundMintingDelay: null, }; } @@ -450,7 +453,18 @@ export function getExpectedAmuletRulesConfigDiffsHTML( class="jsondiffpatch-unchanged" data-key="transferPreapprovalBaseDuration">
transferPreapprovalBaseDuration
null
+ class="jsondiffpatch-value">
null
  • developmentFundManagerBlacklist
    null
  • minDevelopmentFundMintingDelay
    null
  • amuletSwitchOverTimes
    null
  • `; return mock; } diff --git a/apps/common/frontend-test-handlers/src/mocks/helpers/dso-config-helper.ts b/apps/common/frontend-test-handlers/src/mocks/helpers/dso-config-helper.ts index a02e2fe3a7d..fed4275d77f 100644 --- a/apps/common/frontend-test-handlers/src/mocks/helpers/dso-config-helper.ts +++ b/apps/common/frontend-test-handlers/src/mocks/helpers/dso-config-helper.ts @@ -21,6 +21,7 @@ export function getDsoRulesConfig( ? nextScheduledSynchronizerUpgrade : null, nextScheduledLogicalSynchronizerUpgrade: null, + svOperationsSwitchOverTimes: null, actionConfirmationTimeout: { microseconds: '3600000000', }, @@ -224,6 +225,9 @@ export function getExpectedDsoRulesConfigDiffsHTML( }
  • nextScheduledLogicalSynchronizerUpgrade
    null
  • svOperationsSwitchOverTimes
    null
  • `; @@ -325,6 +329,9 @@ export function getExpectedDsoRulesConfigDiffsHTML( }
  • nextScheduledLogicalSynchronizerUpgrade
    null
  • svOperationsSwitchOverTimes
    null
  • `; diff --git a/apps/common/frontend-test-vite-utils/src/vitest.common.conf.ts b/apps/common/frontend-test-vite-utils/src/vitest.common.conf.ts deleted file mode 100644 index 262be099207..00000000000 --- a/apps/common/frontend-test-vite-utils/src/vitest.common.conf.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const vitest_common_conf = { - test: { - environment: 'happy-dom', - typecheck: { - include: ['**/*.{test,spec}-d.?(c|m)[jt]s?(x)', '**/*.{test,spec}.?(c|m)[jt]s?(x)'], - }, - exclude: ['../lib/**'], - silent: false, - disableConsoleIntercept: true, - }, -}; diff --git a/apps/common/frontend/src/__tests__/mocks/constants.ts b/apps/common/frontend/src/__tests__/mocks/constants.ts index 71c1e81aff7..56b6abc951e 100644 --- a/apps/common/frontend/src/__tests__/mocks/constants.ts +++ b/apps/common/frontend/src/__tests__/mocks/constants.ts @@ -196,6 +196,9 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { externalPartyConfigStateTickDuration: null, rewardConfig: null, transferPreapprovalBaseDuration: null, + developmentFundManagerBlacklist: null, + minDevelopmentFundMintingDelay: null, + amuletSwitchOverTimes: null, }, }, }, diff --git a/apps/common/frontend/src/utils/helpers.ts b/apps/common/frontend/src/utils/helpers.ts index 2c01e5bd143..32c0ce7a83d 100644 --- a/apps/common/frontend/src/utils/helpers.ts +++ b/apps/common/frontend/src/utils/helpers.ts @@ -69,7 +69,18 @@ export const isUnauthorizedError = (error: Error): boolean => { return err.code === 401 || err.status === 401; }; +export const isRateLimitedError = (error: Error): boolean => { + const err = error as { code?: unknown; status?: unknown }; + return err.code === 429 || err.status === 429; +}; + +const RATE_LIMIT_RETRY_CAP = 5; + +export const retryOnRateLimit = (failureCount: number, error: Error): boolean => + isRateLimitedError(error) && failureCount < RATE_LIMIT_RETRY_CAP; + export const retryQuery = (failureCount: number, error: Error): boolean => { if (isUnauthorizedError(error)) return false; + if (isRateLimitedError(error)) return failureCount < RATE_LIMIT_RETRY_CAP; return failureCount < 3; }; diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala index e2ec3530cf5..038bdbc5f3c 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/automation/BatchedMultiDomainExpiredContractTrigger.scala @@ -49,8 +49,7 @@ abstract class BatchedMultiDomainExpiredContractTrigger[ protected def ignorePartiesWithoutVettedAmulet( informees: Set[PartyId], contractIds: Seq[String], - logAsWarning: Boolean, - )(implicit tc: TraceContext): String + ): String override final protected def listReadyTasks(now: CantonTimestamp, limit: Int)(implicit tc: TraceContext @@ -79,7 +78,6 @@ abstract class BatchedMultiDomainExpiredContractTrigger[ ignorePartiesWithoutVettedAmulet( stakeholders, contracts.flatten.map(_.contractId.contractId), - logAsWarning = true, ).discard Seq.empty } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala index 39626cd40f3..6623a18e723 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/RateLimitersConfig.scala @@ -3,18 +3,35 @@ package org.lfdecentralizedtrust.splice.config -import org.lfdecentralizedtrust.splice.util.{PerAttributeRateLimitConfig, SpliceRateLimitConfig} +import org.lfdecentralizedtrust.splice.util.{ + PerAttributeRateLimitConfig, + SpliceRateLimitConfig, + SpliceRateLimiter, +} + +/** An overall rate limit that additionally limits per client IP. + * + * The `attributeOverrides` of `perClientIp` (`ip-overrides` in the config) are keyed by an IP + * network in CIDR notation (a bare IP address denotes a single host), e.g. + * `{ "10.0.0.0/8" = { rate-per-second = 100 } }`. + */ +case class PerClientIpRateLimitConfig( + enabled: Boolean = true, + ratePerSecond: Double, + sustainedRatePerSecond: Option[Double] = None, + sustainedWindowSeconds: Long = SpliceRateLimiter.DefaultSustainedWindowSeconds, + perClientIp: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig.disabled, +) extends SpliceRateLimitConfig case class RateLimitersConfig( /** Overall rate limiter applied per operation. Used when there is no operation-specific override * in `rateLimiters`. The embedded `perClientIp` limiter is disabled by default; enable it to * additionally limit per client IP. */ - default: SpliceRateLimitConfig.WithPerClientIp = - SpliceRateLimitConfig.WithPerClientIp(ratePerSecond = 200), + default: PerClientIpRateLimitConfig = PerClientIpRateLimitConfig(ratePerSecond = 200), /** Per-operation overrides of the overall `default` rate limiter. */ - rateLimiters: Map[String, SpliceRateLimitConfig.WithPerClientIp] = Map.empty, - global: SpliceRateLimitConfig.WithPerClientIp = RateLimitersConfig.DefaultGlobal, + rateLimiters: Map[String, PerClientIpRateLimitConfig] = Map.empty, + global: PerClientIpRateLimitConfig = RateLimitersConfig.DefaultGlobal, /** Names of the HTTP headers from which the client IP used for per-client-IP rate limiting is * extracted, in order of precedence: the first header that is present and whose value (or, for * comma separated lists such as `X-Forwarded-For`, whose first entry) parses as an IP literal @@ -27,7 +44,7 @@ case class RateLimitersConfig( */ clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders, ) { - def forRateLimiter(name: String): SpliceRateLimitConfig.WithPerClientIp = + def forRateLimiter(name: String): PerClientIpRateLimitConfig = rateLimiters.getOrElse(name, default) } @@ -39,8 +56,8 @@ object RateLimitersConfig { */ val DefaultClientIpHeaders: Seq[String] = Seq("x-forwarded-for", "x-real-ip") - private val DefaultGlobal: SpliceRateLimitConfig.WithPerClientIp = - SpliceRateLimitConfig.WithPerClientIp( + private val DefaultGlobal: PerClientIpRateLimitConfig = + PerClientIpRateLimitConfig( ratePerSecond = 200, perClientIp = PerAttributeRateLimitConfig(), ) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala index ee0a4e30ddd..17a70879d08 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala @@ -1361,12 +1361,68 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val utilTokenStandardWallet_1_1_1 = DarResource( + "splice-util-token-standard-wallet-1.1.1.dar", + "5acc49b7a6a41f6a3edbf9d7f9efae83cb2d97fb0ad67de52b2052922fa1d32d", + PackageMetadata( + PackageName.assertFromString("splice-util-token-standard-wallet"), + PackageVersion.assertFromString("1.1.1"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val utilTokenStandardWallet_current = DarResource( "splice-util-token-standard-wallet-current.dar", - "1215d3ee8f3cb428d062d8708c2deca33c91ff99f5edefa70616646e57f93012", + "5acc49b7a6a41f6a3edbf9d7f9efae83cb2d97fb0ad67de52b2052922fa1d32d", PackageMetadata( PackageName.assertFromString("splice-util-token-standard-wallet"), - PackageVersion.assertFromString("1.1.0"), + PackageVersion.assertFromString("1.1.1"), None, ), Set( @@ -1424,6 +1480,7 @@ object DarResources { utilTokenStandardWallet_1_0_0, utilTokenStandardWallet_1_0_1, utilTokenStandardWallet_1_1_0, + utilTokenStandardWallet_1_1_1, ), ) val tokenTestTradingApp_1_0_0 = DarResource( @@ -1562,12 +1619,58 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val tokenTestTradingApp_1_0_3 = DarResource( + "splice-token-test-trading-app-1.0.3.dar", + "76913ca0dc24309b720e6b0133363f40b30b78433d581d46f4a9cc8b61715eca", + PackageMetadata( + PackageName.assertFromString("splice-token-test-trading-app"), + PackageVersion.assertFromString("1.0.3"), + None, + ), + Set( + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val tokenTestTradingApp_current = DarResource( "splice-token-test-trading-app-current.dar", - "88988edeabc5fcea383d5d0a7333c1f71468be4e9e1684a640f766eca32a0ed1", + "76913ca0dc24309b720e6b0133363f40b30b78433d581d46f4a9cc8b61715eca", PackageMetadata( PackageName.assertFromString("splice-token-test-trading-app"), - PackageVersion.assertFromString("1.0.2"), + PackageVersion.assertFromString("1.0.3"), None, ), Set( @@ -1611,7 +1714,12 @@ object DarResources { val tokenTestTradingApp = PackageResource( tokenTestTradingApp_current, tokenTestTradingApp_1_0_0, - Seq(tokenTestTradingApp_1_0_0, tokenTestTradingApp_1_0_1, tokenTestTradingApp_1_0_2), + Seq( + tokenTestTradingApp_1_0_0, + tokenTestTradingApp_1_0_1, + tokenTestTradingApp_1_0_2, + tokenTestTradingApp_1_0_3, + ), ) val tokenTestTradingAppV2_1_0_0 = DarResource( "splice-token-test-trading-app-v2-1.0.0.dar", @@ -1668,12 +1776,67 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val tokenTestTradingAppV2_1_0_1 = DarResource( + "splice-token-test-trading-app-v2-1.0.1.dar", + "c01d9742db7c7c515dcd8cb2ff4b186c0957bfc39f6020db424b1cc80d08c8e4", + PackageMetadata( + PackageName.assertFromString("splice-token-test-trading-app-v2"), + PackageVersion.assertFromString("1.0.1"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val tokenTestTradingAppV2_current = DarResource( "splice-token-test-trading-app-v2-current.dar", - "20d42271cbd760c26c08401e4d1eec624458f5b48c73ae44c87c754e35621f5d", + "c01d9742db7c7c515dcd8cb2ff4b186c0957bfc39f6020db424b1cc80d08c8e4", PackageMetadata( PackageName.assertFromString("splice-token-test-trading-app-v2"), - PackageVersion.assertFromString("1.0.0"), + PackageVersion.assertFromString("1.0.1"), None, ), Set( @@ -1726,7 +1889,7 @@ object DarResources { val tokenTestTradingAppV2 = PackageResource( tokenTestTradingAppV2_current, tokenTestTradingAppV2_1_0_0, - Seq(tokenTestTradingAppV2_1_0_0), + Seq(tokenTestTradingAppV2_1_0_0, tokenTestTradingAppV2_1_0_1), ) val allProductionPackageResources = Seq( apiTokenMetadataV1, @@ -2806,18 +2969,80 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val amulet_0_1_23 = DarResource( + "splice-amulet-0.1.23.dar", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", + PackageMetadata( + PackageName.assertFromString("splice-amulet"), + PackageVersion.assertFromString("0.1.23"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val amulet_current = DarResource( "splice-amulet-current.dar", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", PackageMetadata( PackageName.assertFromString("splice-amulet"), - PackageVersion.assertFromString("0.1.22"), + PackageVersion.assertFromString("0.1.23"), None, ), Set( "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -2827,6 +3052,7 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", @@ -2837,8 +3063,6 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", @@ -2869,7 +3093,7 @@ object DarResources { ) val amulet = PackageResource( amulet_current, - amulet_0_1_15, + amulet_0_1_19, Seq( amulet_0_1_0, amulet_0_1_1, @@ -2894,6 +3118,7 @@ object DarResources { amulet_0_1_20, amulet_0_1_21, amulet_0_1_22, + amulet_0_1_23, ), ) val dsoGovernance_0_1_0 = DarResource( @@ -4303,20 +4528,83 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val dsoGovernance_0_1_29 = DarResource( + "splice-dso-governance-0.1.29.dar", + "2b680f34f11be43d65a2dde723f4c9595a0983f63a0bb3455f03bb16c5d9b814", + PackageMetadata( + PackageName.assertFromString("splice-dso-governance"), + PackageVersion.assertFromString("0.1.29"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "cc7d11e790174d2b18ad3e148d8762340e58de35616b399f9397a1b1d5b752f1", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val dsoGovernance_current = DarResource( "splice-dso-governance-current.dar", - "eeb461e9e430d8aaaa232f23058a28c9858d559abd083c391d7ab7e73efb4e13", + "2b680f34f11be43d65a2dde723f4c9595a0983f63a0bb3455f03bb16c5d9b814", PackageMetadata( PackageName.assertFromString("splice-dso-governance"), - PackageVersion.assertFromString("0.1.28"), + PackageVersion.assertFromString("0.1.29"), None, ), Set( - "9cffe65feb664c9550937433067e9f969e3795c6fb38715e06a5e04fc1ae1f83", - "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -4326,8 +4614,10 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "cc7d11e790174d2b18ad3e148d8762340e58de35616b399f9397a1b1d5b752f1", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", @@ -4336,13 +4626,13 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", @@ -4361,7 +4651,6 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", @@ -4369,7 +4658,7 @@ object DarResources { ) val dsoGovernance = PackageResource( dsoGovernance_current, - dsoGovernance_0_1_21, + dsoGovernance_0_1_25, Seq( dsoGovernance_0_1_0, dsoGovernance_0_1_1, @@ -4400,6 +4689,7 @@ object DarResources { dsoGovernance_0_1_26, dsoGovernance_0_1_27, dsoGovernance_0_1_28, + dsoGovernance_0_1_29, ), ) val utilBatchedMarkers_1_0_0 = DarResource( @@ -4573,12 +4863,56 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val utilBatchedMarkers_1_0_4 = DarResource( + "splice-util-batched-markers-1.0.4.dar", + "c442a23fc47319108d6408ca0386b6087c255f79bf180a6aa77a807a985085c2", + PackageMetadata( + PackageName.assertFromString("splice-util-batched-markers"), + PackageVersion.assertFromString("1.0.4"), + None, + ), + Set( + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val utilBatchedMarkers_current = DarResource( "splice-util-batched-markers-current.dar", - "74ee4c91b80c80b6860005ce627bdaed8f7fe6bdf3e60c9dbf3f48402d55126a", + "c442a23fc47319108d6408ca0386b6087c255f79bf180a6aa77a807a985085c2", PackageMetadata( PackageName.assertFromString("splice-util-batched-markers"), - PackageVersion.assertFromString("1.0.3"), + PackageVersion.assertFromString("1.0.4"), None, ), Set( @@ -4625,6 +4959,7 @@ object DarResources { utilBatchedMarkers_1_0_1, utilBatchedMarkers_1_0_2, utilBatchedMarkers_1_0_3, + utilBatchedMarkers_1_0_4, ), ) val wallet_0_1_0 = DarResource( @@ -5695,7 +6030,69 @@ object DarResources { "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", - "dc1a2f4ff4774e00c388f1bfce48a23c3539b8ea0d0ebf99b8df840a058ef873", + "dc1a2f4ff4774e00c388f1bfce48a23c3539b8ea0d0ebf99b8df840a058ef873", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) + val wallet_0_1_23 = DarResource( + "splice-wallet-0.1.23.dar", + "727c0fb9711d59b506fbb3cff2342e55eb583bb88682a946220525c304feca4a", + PackageMetadata( + PackageName.assertFromString("splice-wallet"), + PackageVersion.assertFromString("0.1.23"), + None, + ), + Set( + "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", @@ -5713,24 +6110,25 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) - val wallet_0_1_23 = DarResource( - "splice-wallet-0.1.23.dar", - "727c0fb9711d59b506fbb3cff2342e55eb583bb88682a946220525c304feca4a", + val wallet_0_1_24 = DarResource( + "splice-wallet-0.1.24.dar", + "d42261314d98cc04bf50bcc58930af98b9f33aeed11339465925de697d3ba66e", PackageMetadata( PackageName.assertFromString("splice-wallet"), - PackageVersion.assertFromString("0.1.23"), + PackageVersion.assertFromString("0.1.24"), None, ), Set( - "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -5740,6 +6138,7 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", @@ -5750,13 +6149,13 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", @@ -5775,7 +6174,6 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", @@ -5783,17 +6181,17 @@ object DarResources { ) val wallet_current = DarResource( "splice-wallet-current.dar", - "727c0fb9711d59b506fbb3cff2342e55eb583bb88682a946220525c304feca4a", + "d42261314d98cc04bf50bcc58930af98b9f33aeed11339465925de697d3ba66e", PackageMetadata( PackageName.assertFromString("splice-wallet"), - PackageVersion.assertFromString("0.1.23"), + PackageVersion.assertFromString("0.1.24"), None, ), Set( - "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -5803,6 +6201,7 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", @@ -5813,13 +6212,13 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", @@ -5838,7 +6237,6 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", @@ -5846,7 +6244,7 @@ object DarResources { ) val wallet = PackageResource( wallet_current, - wallet_0_1_15, + wallet_0_1_20, Seq( wallet_0_1_0, wallet_0_1_1, @@ -5872,6 +6270,7 @@ object DarResources { wallet_0_1_21, wallet_0_1_22, wallet_0_1_23, + wallet_0_1_24, ), ) val amuletNameService_0_1_0 = DarResource( @@ -7028,19 +7427,82 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val amuletNameService_0_1_24 = DarResource( + "splice-amulet-name-service-0.1.24.dar", + "cc7d11e790174d2b18ad3e148d8762340e58de35616b399f9397a1b1d5b752f1", + PackageMetadata( + PackageName.assertFromString("splice-amulet-name-service"), + PackageVersion.assertFromString("0.1.24"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val amuletNameService_current = DarResource( "splice-amulet-name-service-current.dar", - "9cffe65feb664c9550937433067e9f969e3795c6fb38715e06a5e04fc1ae1f83", + "cc7d11e790174d2b18ad3e148d8762340e58de35616b399f9397a1b1d5b752f1", PackageMetadata( PackageName.assertFromString("splice-amulet-name-service"), - PackageVersion.assertFromString("0.1.23"), + PackageVersion.assertFromString("0.1.24"), None, ), Set( - "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -7050,6 +7512,7 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", @@ -7060,13 +7523,13 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", @@ -7085,7 +7548,6 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", @@ -7093,7 +7555,7 @@ object DarResources { ) val amuletNameService = PackageResource( amuletNameService_current, - amuletNameService_0_1_16, + amuletNameService_0_1_20, Seq( amuletNameService_0_1_0, amuletNameService_0_1_1, @@ -7119,6 +7581,7 @@ object DarResources { amuletNameService_0_1_21, amuletNameService_0_1_22, amuletNameService_0_1_23, + amuletNameService_0_1_24, ), ) val walletPayments_0_1_0 = DarResource( @@ -8203,18 +8666,81 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val walletPayments_0_1_23 = DarResource( + "splice-wallet-payments-0.1.23.dar", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", + PackageMetadata( + PackageName.assertFromString("splice-wallet-payments"), + PackageVersion.assertFromString("0.1.23"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val walletPayments_current = DarResource( "splice-wallet-payments-current.dar", - "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", PackageMetadata( PackageName.assertFromString("splice-wallet-payments"), - PackageVersion.assertFromString("0.1.22"), + PackageVersion.assertFromString("0.1.23"), None, ), Set( "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -8224,6 +8750,7 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", @@ -8234,11 +8761,10 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", @@ -8259,7 +8785,6 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", @@ -8267,7 +8792,7 @@ object DarResources { ) val walletPayments = PackageResource( walletPayments_current, - walletPayments_0_1_15, + walletPayments_0_1_19, Seq( walletPayments_0_1_0, walletPayments_0_1_1, @@ -8292,6 +8817,7 @@ object DarResources { walletPayments_0_1_20, walletPayments_0_1_21, walletPayments_0_1_22, + walletPayments_0_1_23, ), ) val splitwell_0_1_0 = DarResource( @@ -9448,19 +9974,82 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val splitwell_0_1_24 = DarResource( + "splitwell-0.1.24.dar", + "f2ff9522cd65a8ed15c62c709a37e027fcd879ec5eab9e1476bde67b33deaec8", + PackageMetadata( + PackageName.assertFromString("splitwell"), + PackageVersion.assertFromString("0.1.24"), + None, + ), + Set( + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", + "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "adc16315a8943a8433886694720a2a000ae84c2315c4414bd6d0db4d1660de9c", + "dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d", + "3b25c9b08ac6d895417c604fc0ee4b7e47ef974ff8fa43f139daa43bb431fefc", + "718a0f77e505a8de22f188bd4c87fe74101274e9d4cb1bfac7d09aec7158d35b", + "5c1097a9bad0af4bcfe6d3fb0fe55112d3d11f18eae57ddfb14c20836fee226c", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "4b7ecfc366d79ccc5ed07c80f26fe489cf2dfd43ce2856c06a78e6a048db7032", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "051a3b0563a6fa4df4cb34448081e48b061e555aa1a265abf6ae8f3f4cafe439", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "9d1a644e686435cf934f2f3f049138a424863b2a800130046dadc0c994ea5df3", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520", + "9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val splitwell_current = DarResource( "splitwell-current.dar", - "68d3911f72347dc787c336f73af3c036004c982f591cb55e7b4f2fb45f5e50e6", + "f2ff9522cd65a8ed15c62c709a37e027fcd879ec5eab9e1476bde67b33deaec8", PackageMetadata( PackageName.assertFromString("splitwell"), - PackageVersion.assertFromString("0.1.23"), + PackageVersion.assertFromString("0.1.24"), None, ), Set( - "866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616", "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", "4ded6b668cb3b64f7a88a30874cd41c75829f5e064b3fbbadf41ec7e8363354f", + "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "29317e3b7b165d2bbf16721bcca0ec4869e53eddb2738bddf790d61af28e0099", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "7cff38e34bd192d498d5a7606aa3b893e35b0f632d582b273f66dab90f2f14ef", @@ -9470,6 +10059,7 @@ object DarResources { "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda", "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", @@ -9480,13 +10070,13 @@ object DarResources { "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", - "54f85ebfc7dfae18f7d70370015dcc6c6792f60135ab369c44ae52c6fc17c274", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", "6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35", + "8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a", "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a", "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", @@ -9505,7 +10095,6 @@ object DarResources { "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", "55ba4deb0ad4662c4168b39859738a0e91388d252286480c7331b3f71a517281", - "fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a", "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", "9818a0b5b827109de03a04c8f6151cde9d1e7fe5123dbb2dfeb0e52d7271287c", "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", @@ -9513,7 +10102,7 @@ object DarResources { ) val splitwell = PackageResource( splitwell_current, - splitwell_0_1_15, + splitwell_0_1_20, Seq( splitwell_0_1_0, splitwell_0_1_1, @@ -9539,6 +10128,7 @@ object DarResources { splitwell_0_1_21, splitwell_0_1_22, splitwell_0_1_23, + splitwell_0_1_24, ), ) val validatorLifecycle_0_1_0 = DarResource( @@ -9907,12 +10497,53 @@ object DarResources { "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", ), ) + val validatorLifecycle_0_1_9 = DarResource( + "splice-validator-lifecycle-0.1.9.dar", + "639d2fd54431a2432bc9394917b1a6f664e79cfb898cd5487462b6ef8a282798", + PackageMetadata( + PackageName.assertFromString("splice-validator-lifecycle"), + PackageVersion.assertFromString("0.1.9"), + None, + ), + Set( + "b70db8369e1c461d5c70f1c86f526a29e9776c655e6ffc2560f95b05ccb8b946", + "52854220dc199884704958df38befd5492d78384a032fd7558c38f00e3d778a2", + "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", + "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", + "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", + "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", + "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", + "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", + "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", + "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", + "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", + "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", + "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", + "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", + "fcee8dfc1b81c449b421410edd5041c16ab59c45bbea85bcb094d1b17c3e9df7", + "6f8e6085f5769861ae7a40dccd618d6f747297d59b37cab89b93e2fa80b0c024", + "fa79192fe1cce03d7d8db36471dde4cf6c96e6d0f07e1c391dd49e355af9b38c", + "19f0df5fdaf5a96e137b6ea885fdb378f37bd3166bd9a47ee11518e33fa09a20", + "a1fa18133ae48cbb616c4c148e78e661666778c3087d099067c7fe1868cbb3a1", + "86d888f34152dae8729900966b44abcb466b9c111699678de58032de601d2b04", + "6da1f43a10a179524e840e7288b47bda213339b0552d92e87ae811e52f59fc0e", + "22ffcbab726e64e179deebd895307fad94efbae3f72b07677e24038d0fbf4467", + "3cde94fe9be5c700fc1d9a8ad2277e2c1214609f8c52a5b4db77e466875b8cb7", + "bfda48f9aa2c89c895cde538ec4b4946c7085959e031ad61bde616b9849155d7", + "ee33fb70918e7aaa3d3fc44d64a399fb2bf5bcefc54201b1690ecd448551ba88", + "e5411f3d75f072b944bd88e652112a14a3d409c491fd9a51f5f6eede6d3a3348", + "5aee9b21b8e9a4c4975b5f4c4198e6e6e8469df49e2010820e792f393db870f4", + "cae345b5500ef6f84645c816f88b9f7a85a9f3c71697984abdf6849f81e80324", + "91e167fa7a256f21f990c526a0a0df840e99aeef0e67dc1f5415b0309486de74", + ), + ) val validatorLifecycle_current = DarResource( "splice-validator-lifecycle-current.dar", - "217edf88c015ee080de2132a208cf23c14e9a2773198c0d4078985ab25ad4be1", + "639d2fd54431a2432bc9394917b1a6f664e79cfb898cd5487462b6ef8a282798", PackageMetadata( PackageName.assertFromString("splice-validator-lifecycle"), - PackageVersion.assertFromString("0.1.8"), + PackageVersion.assertFromString("0.1.9"), None, ), Set( @@ -9921,13 +10552,13 @@ object DarResources { "9e70a8b3510d617f8a136213f33d6a903a10ca0eeec76bb06ba55d1ed9680f69", "d095a2ccf6dd36b2415adc4fa676f9191ba63cd39828dc5207b36892ec350cbc", "7adc4c2d07fa3a51173c843cba36e610c1168b2dbbf53076e20c0092eae8763d", - "b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc", "4039f60273388c5701e27950ab3e8c3044433b0876535a3b48fb042cc77bf02a", "bde4bd30749e99603e5afa354706608601029e225d4983324d617825b634253a", "c280cc3ef501d237efa7b1120ca3ad2d196e089ad596b666bed59a85f3c9a074", "c3bb0c5d04799b3f11bad7c3c102963e115cf53da3e4afcbcfd9f06ebd82b4ff", "0e4a572ab1fb94744abb02243a6bbed6c78fc6e3c8d3f60c655f057692a62816", "ab068e2f920d0e06347975c2a342b71f8b8e3b4be0f02ead9442caac51aa8877", + "411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124", "e7e0adfa881e7dbbb07da065ae54444da7c4bccebcb8872ab0cb5dcf9f3761ce", "f181cd661f7af3a60bdaae4b0285a2a67beb55d6910fc8431dbae21a5825ec0f", "60c61c542207080e97e378ab447cc355ecc47534b3a3ebbff307c4fb8339bc4d", @@ -9961,6 +10592,7 @@ object DarResources { validatorLifecycle_0_1_6, validatorLifecycle_0_1_7, validatorLifecycle_0_1_8, + validatorLifecycle_0_1_9, ), ) val apiRewardAssignmentV1_1_0_0 = DarResource( diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageIdResolver.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageIdResolver.scala index 991d1f696fa..0cfa2f7ce1f 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageIdResolver.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageIdResolver.scala @@ -70,6 +70,9 @@ object PackageIdResolver { // Batched markers case SpliceUtilBatchedMarkers => DarResources.utilBatchedMarkers.latest.metadata.version.toString() + // Reward assignment + case SpliceApiRewardAssignmentV1 => + DarResources.apiRewardAssignmentV1.latest.metadata.version.toString() } PackageVersion.assertFromString(version) } @@ -129,6 +132,7 @@ object PackageIdResolver { final case object SpliceWallet extends Package final case object SpliceWalletPayments extends Package final case object SpliceUtilBatchedMarkers extends Package + final case object SpliceApiRewardAssignmentV1 extends Package } val validatorPackages = Set( diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageVersionSupport.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageVersionSupport.scala index 7650689e2d7..e3b6d179c71 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageVersionSupport.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageVersionSupport.scala @@ -69,6 +69,7 @@ trait PackageVersionSupport extends NamedLogging { ) } + // TODO(#6388): remove redundant version check def supportsConvertFeaturedAppActivityMarkerObservers( parties: Seq[PartyId], now: CantonTimestamp, @@ -79,8 +80,10 @@ trait PackageVersionSupport extends NamedLogging { now, DarResources.amulet, DarResources.amulet_0_1_16, + ignoreRedundantCheck = true, ) + // TODO(#6388): remove redundant version check def supports24hSubmissionDelay( amuletParties: Seq[PartyId], dsoGovernanceParties: Seq[PartyId], @@ -96,7 +99,7 @@ trait PackageVersionSupport extends NamedLogging { now, DarResources.amulet, DarResources.amulet_0_1_17, - ignoreRedundantCheck = false, + ignoreRedundantCheck = true, ) } @@ -135,6 +138,7 @@ trait PackageVersionSupport extends NamedLogging { tc: TraceContext ): Future[FeatureSupport] = supports24hSubmissionDelay(amuletParties, dsoGovernanceParties, now) + // TODO(#6388): remove redundant version check def supports24hSubmissionDelayDsoGovernance(parties: Seq[PartyId], now: CantonTimestamp)(implicit tc: TraceContext ): Future[FeatureSupport] = { @@ -144,9 +148,11 @@ trait PackageVersionSupport extends NamedLogging { now, DarResources.dsoGovernance, DarResources.dsoGovernance_0_1_23, + ignoreRedundantCheck = true, ) } + // TODO(#6388): remove redundant version check // TODO(#564) - ensure the right version is used def supportsPhysicalSynchronizers(parties: Seq[PartyId], now: CantonTimestamp)(implicit tc: TraceContext @@ -157,9 +163,11 @@ trait PackageVersionSupport extends NamedLogging { now, DarResources.dsoGovernance, DarResources.dsoGovernance_0_1_24, + ignoreRedundantCheck = true, ) } + // TODO(#6388): remove redundant version check def supportsTrafficBasedAppRewards(parties: Seq[PartyId], now: CantonTimestamp)(implicit tc: TraceContext ): Future[FeatureSupport] = @@ -169,8 +177,10 @@ trait PackageVersionSupport extends NamedLogging { now, DarResources.amulet, DarResources.amulet_0_1_19, + ignoreRedundantCheck = true, ) + // TODO(#6388): remove redundant version check def supportsMintingDelegation(parties: Seq[PartyId], now: CantonTimestamp)(implicit tc: TraceContext ): Future[FeatureSupport] = @@ -180,6 +190,7 @@ trait PackageVersionSupport extends NamedLogging { now, DarResources.wallet, DarResources.wallet_0_1_16, + ignoreRedundantCheck = true, ) private def isDarSupported( @@ -188,7 +199,7 @@ trait PackageVersionSupport extends NamedLogging { at: CantonTimestamp, packageResource: PackageResource, dar: DarResource, - ignoreRedundantCheck: Boolean = false, + ignoreRedundantCheck: Boolean, )(implicit tc: TraceContext): Future[FeatureSupport] = isDarSupported(Seq(packageId -> parties), at, packageResource, dar, ignoreRedundantCheck) diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala index 4cf216ee814..b77850b1b8e 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiter.scala @@ -10,6 +10,7 @@ import org.apache.pekko.http.scaladsl.model.{HttpEntity, RemoteAddress, StatusCo import org.apache.pekko.http.scaladsl.server.{Directive0, Directive1} import org.lfdecentralizedtrust.splice.config.RateLimitersConfig import org.lfdecentralizedtrust.splice.util.{ + IpCidrRateLimits, PerAttributeRateLimiter, SpliceRateLimiter, SpliceRateLimitMetrics, @@ -35,6 +36,9 @@ class HttpRateLimiter( private val clientIpHeaders: Seq[String] = config.clientIpHeaders.map(_.trim).filter(_.nonEmpty) + private val clientIpKey: Directive1[Option[String]] = + HttpRateLimiter.extractClientIpKey(clientIpHeaders) + private def metricsFor(service: String): SpliceRateLimitMetrics = metrics.getOrElseUpdate( service, @@ -56,10 +60,10 @@ class HttpRateLimiter( new PerAttributeRateLimiter( HttpRateLimiter.GlobalLimiter, HttpRateLimiter.ClientIpAttribute, - config.global, config.global.perClientIp, globalMetrics, logger, + IpCidrRateLimits.matchClientIp, ), ) } @@ -81,10 +85,10 @@ class HttpRateLimiter( new PerAttributeRateLimiter( operation, HttpRateLimiter.ClientIpAttribute, - operationConfig, operationConfig.perClientIp, rateLimiterMetrics, logger, + IpCidrRateLimits.matchClientIp, ), ) }, @@ -96,8 +100,7 @@ class HttpRateLimiter( import org.apache.pekko.http.scaladsl.server.Directives.* - HttpRateLimiter - .extractClientIpKey(clientIpHeaders) + clientIpKey .flatMap { clientIp => // The per client IP limiters are checked first (and `&&` short-circuits) so that a request // rejected because of its own client IP does not consume budget from the shared overall diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala index cf5227001a1..e9d5c79e5ee 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/DbVotesStoreQueryBuilder.scala @@ -5,8 +5,10 @@ package org.lfdecentralizedtrust.splice.store import cats.data.NonEmptyList import com.daml.ledger.javaapi.data.codegen.ContractId +import com.daml.nonempty.NonEmpty import com.digitalasset.canton.config.CantonRequireTypes.String3 import com.digitalasset.canton.logging.NamedLogging +import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.resource.DbStorage.Implicits.BuilderChain.toSQLActionBuilderChain import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.VoteRequest import org.lfdecentralizedtrust.splice.store.db.AcsQueries.AcsStoreId @@ -145,14 +147,14 @@ trait DbVotesAcsStoreQueryBuilder extends AcsQueries with LimitHelpers with Name acsStoreId: AcsStoreId, domainMigrationId: Long, trackingCidColumnName: String, - trackingCids: Seq[VoteRequest.ContractId], + trackingCids: NonEmpty[Seq[VoteRequest.ContractId]], limit: Limit, ): SqlStreamingAction[Vector[ AcsQueries.SelectFromAcsTableResult ], AcsQueries.SelectFromAcsTableResult, Effect.Read] = { - val cids: Seq[ContractId[?]] = trackingCids + val cids: NonEmpty[Seq[ContractId[?]]] = trackingCids val voteRequestTrackingCidsSql = - inClause(trackingCidColumnName, cids) + DbStorage.toInClause(trackingCidColumnName, cids) selectFromAcsTable( acsTableName, acsStoreId, diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala index 3554789a012..57e4376f2e5 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/UpdateHistory.scala @@ -9,6 +9,7 @@ import com.daml.ledger.api.v2.TraceContextOuterClass import com.daml.ledger.javaapi.data.codegen.{ContractId, DamlRecord} import com.daml.ledger.javaapi.data.{CreatedEvent, Event, ExercisedEvent, Identifier, Transaction} import com.daml.metrics.api.MetricsContext +import com.daml.nonempty.NonEmpty import com.google.protobuf.ByteString import com.digitalasset.canton.util.HexString import org.lfdecentralizedtrust.splice.environment.ledger.api.ReassignmentEvent.{Assign, Unassign} @@ -1339,33 +1340,33 @@ class UpdateHistory( private def queryCreateEvents( transactionRowIds: Seq[Long] )(implicit tc: TraceContext): Future[Map[Long, Seq[SelectFromCreateEvents]]] = { - if (transactionRowIds.isEmpty) { - Future.successful(Map.empty) - } else { - storage - .query( - (sql""" - select - update_row_id, - event_id, - contract_id, - created_at, - template_id_package_id, - template_id_module_name, - template_id_entity_name, - package_name, - create_arguments, - signatories, - observers, - contract_key, - record_time - - from update_history_creates - where """ ++ inClause("update_row_id", transactionRowIds)).toActionBuilder - .as[SelectFromCreateEvents], - "queryCreateEvents", - ) - .map(_.groupBy(_.updateRowId)) + NonEmpty.from(transactionRowIds) match { + case None => Future.successful(Map.empty) + case Some(transactionRowIds) => + storage + .query( + (sql""" + select + update_row_id, + event_id, + contract_id, + created_at, + template_id_package_id, + template_id_module_name, + template_id_entity_name, + package_name, + create_arguments, + signatories, + observers, + contract_key, + record_time + + from update_history_creates + where """ ++ DbStorage.toInClause("update_row_id", transactionRowIds)).toActionBuilder + .as[SelectFromCreateEvents], + "queryCreateEvents", + ) + .map(_.groupBy(_.updateRowId)) } } @@ -1410,35 +1411,35 @@ class UpdateHistory( private def queryExerciseEvents( transactionRowIds: Seq[Long] )(implicit tc: TraceContext): Future[Map[Long, Seq[SelectFromExerciseEvents]]] = { - if (transactionRowIds.isEmpty) { - Future.successful(Map.empty) - } else { - storage - .query( - (sql""" - select - update_row_id, - event_id, - child_event_ids, - choice, - template_id_package_id, - template_id_module_name, - template_id_entity_name, - contract_id, - consuming, - package_name, - argument, - result, - acting_parties, - interface_id_package_id, - interface_id_module_name, - interface_id_entity_name - from update_history_exercises - where """ ++ inClause("update_row_id", transactionRowIds)).toActionBuilder - .as[SelectFromExerciseEvents], - "queryExerciseEvents", - ) - .map(_.groupBy(_.updateRowId)) + NonEmpty.from(transactionRowIds) match { + case None => Future.successful(Map.empty) + case Some(transactionRowIds) => + storage + .query( + (sql""" + select + update_row_id, + event_id, + child_event_ids, + choice, + template_id_package_id, + template_id_module_name, + template_id_entity_name, + contract_id, + consuming, + package_name, + argument, + result, + acting_parties, + interface_id_package_id, + interface_id_module_name, + interface_id_entity_name + from update_history_exercises + where """ ++ DbStorage.toInClause("update_row_id", transactionRowIds)).toActionBuilder + .as[SelectFromExerciseEvents], + "queryExerciseEvents", + ) + .map(_.groupBy(_.updateRowId)) } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala index 470d5e445b2..2f3106530e6 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/DbMultiDomainAcsStore.scala @@ -55,7 +55,7 @@ import org.lfdecentralizedtrust.splice.store.db.AcsQueries.{ } import org.lfdecentralizedtrust.splice.store.db.AcsTables.ContractStateRowData import AsUpdateReturning.* -import com.daml.nonempty.NonEmpty +import com.daml.nonempty.{NonEmpty, NonEmptyUtil} import com.digitalasset.canton.data.CantonTimestamp import com.daml.metrics.api.MetricHandle.LabeledMetricsFactory import com.digitalasset.canton.resource.DbStorage.SQLActionBuilderChain @@ -218,22 +218,24 @@ final class DbMultiDomainAcsStore[TXE]( companionClass: ContractCompanion[C, TCid, T], traceContext: TraceContext, ): Future[Seq[ContractWithState[TCid, T]]] = { - if (ids.isEmpty) Future.successful(Seq.empty) - else { - waitUntilAcsIngested { - storage - .query( // index: acs_store_template_sid_mid_cid - selectFromAcsTableWithState( - acsTableName, - acsStoreId, - domainMigrationId, - companion, - additionalWhere = (sql"and " ++ inClause("acs.contract_id", ids)).toActionBuilder, - ), - "lookupContractsById", - ) - .map(result => result.map(contractWithStateFromRow(companion)(_))) - } + NonEmpty.from(ids) match { + case None => Future.successful(Seq.empty) + case Some(ids) => + waitUntilAcsIngested { + storage + .query( // index: acs_store_template_sid_mid_cid + selectFromAcsTableWithState( + acsTableName, + acsStoreId, + domainMigrationId, + companion, + additionalWhere = + (sql"and " ++ DbStorage.toInClause("acs.contract_id", ids)).toActionBuilder, + ), + "lookupContractsById", + ) + .map(result => result.map(contractWithStateFromRow(companion)(_))) + } } } @@ -287,25 +289,26 @@ final class DbMultiDomainAcsStore[TXE]( def containsArchived(ids: Seq[ContractId[?]])(implicit traceContext: TraceContext ): Future[Boolean] = waitUntilAcsIngested { - if (ids.isEmpty) Future.successful(false) - else { - val expectedCount = ids.size - storage - .query( - (sql""" - select count(1) - from #$acsTableName acs - where acs.store_id = $acsStoreId - and acs.migration_id = $domainMigrationId - and """ ++ inClause("acs.contract_id", ids) ++ sql""" - """).toActionBuilder - .as[Int] - .head, - "containsArchived", - ) - .map { count => - count != expectedCount - } + NonEmpty.from(ids) match { + case None => Future.successful(false) + case Some(ids) => + val expectedCount = ids.size + storage + .query( + (sql""" + select count(1) + from #$acsTableName acs + where acs.store_id = $acsStoreId + and acs.migration_id = $domainMigrationId + and """ ++ DbStorage.toInClause("acs.contract_id", ids) ++ sql""" + """).toActionBuilder + .as[Int] + .head, + "containsArchived", + ) + .map { count => + count != expectedCount + } } } @@ -1725,18 +1728,21 @@ final class DbMultiDomainAcsStore[TXE]( private def checkIncompleteReassignments( contractIds: Seq[String] ): DBIOAction[Set[String], NoStream, Effect.Read] = { - if (contractIds.isEmpty) DBIO.successful(Set.empty) - else { - DBIO - .sequence(contractIds.grouped(ingestionConfig.maxLookupsPerStatement).map { contractIds => - (sql""" - select distinct contract_id from incomplete_reassignments - where store_id = $acsStoreId and migration_id = $domainMigrationId and """ ++ inClause( - "contract_id", - contractIds.map(lengthLimited), - )).toActionBuilder.as[String].map(_.toSet) - }) - .map(_.foldLeft(Set.empty[String])(_ ++ _)) + NonEmpty.from(contractIds) match { + case None => DBIO.successful(Set.empty) + case Some(contractIds) => + DBIO + .sequence(contractIds.grouped(ingestionConfig.maxLookupsPerStatement).map { + contractIds => + (sql""" + select distinct contract_id from incomplete_reassignments + where store_id = $acsStoreId and migration_id = $domainMigrationId and """ ++ DbStorage + .toInClause( + "contract_id", + NonEmptyUtil.fromUnsafe(contractIds.map(lengthLimited)), + )).toActionBuilder.as[String].map(_.toSet) + }) + .map(_.foldLeft(Set.empty[String])(_ ++ _)) } } @@ -1929,53 +1935,54 @@ final class DbMultiDomainAcsStore[TXE]( } private def doDeleteContracts(deletes: Seq[Delete], summary: MutableIngestionSummary) = { - if (deletes.isEmpty) DBIO.successful(()) - else { - DBIO.sequence(deletes.grouped(ingestionConfig.maxDeletesPerStatement).map { deletes => - val performDeleteSql = acsArchiveConfigOpt match { - case Some(AcsArchiveConfig(archiveTableName, baseColumns)) => - val valuesPairs = deletes.map { d => - val cid = lengthLimited(d.evt.getContractId) - val archivedAt = CantonTimestamp.assertFromInstant(d.recordTime).toMicros - sql"($cid, $archivedAt)" - } - val valuesClause = sqlCommaSeparated(valuesPairs) - (sql""" - WITH deleted AS ( - DELETE FROM #$acsTableName - USING (VALUES """ ++ valuesClause ++ sql""") AS at(cid, archived_at) - WHERE store_id = $acsStoreId - AND migration_id = $domainMigrationId - AND #$acsTableName.contract_id = at.cid - RETURNING #$baseColumns, at.archived_at - ) - INSERT INTO #$archiveTableName (#$baseColumns, archived_at) - SELECT * FROM deleted - RETURNING contract_id - """).toActionBuilder.as[String] - case None => - val contractIds = deletes.map(d => lengthLimited(d.evt.getContractId)) - (sql"""DELETE FROM #$acsTableName - WHERE store_id = $acsStoreId - AND migration_id = $domainMigrationId - AND """ ++ inClause( - "contract_id", - contractIds, - ) ++ sql" RETURNING contract_id").toActionBuilder - .as[String] - } + NonEmpty.from(deletes) match { + case None => DBIO.successful(()) + case Some(deletes) => + DBIO.sequence(deletes.grouped(ingestionConfig.maxDeletesPerStatement).map { deletes => + val performDeleteSql = acsArchiveConfigOpt match { + case Some(AcsArchiveConfig(archiveTableName, baseColumns)) => + val valuesPairs = deletes.map { d => + val cid = lengthLimited(d.evt.getContractId) + val archivedAt = CantonTimestamp.assertFromInstant(d.recordTime).toMicros + sql"($cid, $archivedAt)" + } + val valuesClause = sqlCommaSeparated(valuesPairs) + (sql""" + WITH deleted AS ( + DELETE FROM #$acsTableName + USING (VALUES """ ++ valuesClause ++ sql""") AS at(cid, archived_at) + WHERE store_id = $acsStoreId + AND migration_id = $domainMigrationId + AND #$acsTableName.contract_id = at.cid + RETURNING #$baseColumns, at.archived_at + ) + INSERT INTO #$archiveTableName (#$baseColumns, archived_at) + SELECT * FROM deleted + RETURNING contract_id + """).toActionBuilder.as[String] + case None => + val contractIds = deletes.map(d => lengthLimited(d.evt.getContractId)) + (sql"""DELETE FROM #$acsTableName + WHERE store_id = $acsStoreId + AND migration_id = $domainMigrationId + AND """ ++ DbStorage.toInClause( + "contract_id", + NonEmptyUtil.fromUnsafe(contractIds), + ) ++ sql" RETURNING contract_id").toActionBuilder + .as[String] + } - performDeleteSql.map { deletedCids => - val deletedCidSet = deletedCids.toSet - val ingestedArchivedEvents = - deletes.filter(d => deletedCidSet.contains(d.evt.getContractId)).map(_.evt) - summary.ingestedArchivedEvents.addAll(ingestedArchivedEvents) - // there were no contracts with some id. This can happen because: - // `contractFilter.mightContain` in `getIngestionWork` can return true for a template, - // but that might still satisfy some other filter, so the contract was never inserted - summary.numFilteredArchivedEvents += (deletes.length - deletedCids.size) - } - }) + performDeleteSql.map { deletedCids => + val deletedCidSet = deletedCids.toSet + val ingestedArchivedEvents = + deletes.filter(d => deletedCidSet.contains(d.evt.getContractId)).map(_.evt) + summary.ingestedArchivedEvents.addAll(ingestedArchivedEvents) + // there were no contracts with some id. This can happen because: + // `contractFilter.mightContain` in `getIngestionWork` can return true for a template, + // but that might still satisfy some other filter, so the contract was never inserted + summary.numFilteredArchivedEvents += (deletes.length - deletedCids.size) + } + }) } } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala index 1a55b834e82..5718e6b14de 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/store/db/Queries.scala @@ -26,17 +26,6 @@ trait Queries extends JdbcTypes { .getOrElse(SQLActionBuilderChain(sql"")) } - /* - * TODO(#3900) move to use toInClause when canton fork has it: https://github.com/canton-network/splice/issues/3900 - */ - protected def inClause[V: ClassTag]( - field: String, - seq: Iterable[V], - )(implicit - arraySetParameter: SetParameter[Array[V]] - ): SQLActionBuilder = - sql" #$field = ANY(${seq.toArray[V]})" - protected def notInClause[V: ClassTag]( field: String, seq: Iterable[V], diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/IpCidrRateLimits.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/IpCidrRateLimits.scala new file mode 100644 index 00000000000..396facb3ce8 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/IpCidrRateLimits.scala @@ -0,0 +1,141 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.util + +import com.digitalasset.canton.discard.Implicits.DiscardOps +import com.google.common.net.InetAddresses + +import java.net.InetAddress +import scala.annotation.tailrec +import scala.util.Try + +/** An IP network in CIDR notation, e.g. `10.0.0.0/8` or `2001:db8::/32`. A value without a prefix + * length denotes a single host, i.e. a `/32` (IPv4) or `/128` (IPv6) network. + * + * The address is stored as a pair of `Long`s holding the raw address bytes left-aligned in a 128 + * bit big-endian word, with all host bits already zeroed out. This keeps [[IpCidr.contains]] - + * which runs on every request against every configured network - to a handful of allocation-free + * integer operations, and works uniformly for IPv4 (4 bytes) and IPv6 (16 bytes). + * + * @param high + * bytes 0-7 of the network address. + * @param low + * bytes 8-15 of the network address, always 0 for IPv4. + * @param prefixLength + * the number of significant bits, between 0 and `addressBits`. + * @param addressBits + * 32 for IPv4 and 128 for IPv6. + */ +final case class IpCidr(high: Long, low: Long, prefixLength: Int, addressBits: Int) { + + private val maskHigh: Long = IpCidr.mask64(prefixLength) + private val maskLow: Long = IpCidr.mask64(prefixLength - 64) + + /** Whether this network fully contains `other`, which is the case if both are of the same IP + * version, this network is not more specific than `other` and their common prefix matches. + */ + def contains(other: IpCidr): Boolean = + addressBits == other.addressBits && + prefixLength <= other.prefixLength && + (other.high & maskHigh) == high && + (other.low & maskLow) == low + + override def toString: String = { + val bytes = Array.tabulate(addressBits / 8) { index => + (if (index < 8) high >>> (56 - 8 * index) else low >>> (56 - 8 * (index - 8))).toByte + } + s"${InetAddress.getByAddress(bytes).getHostAddress}/$prefixLength" + } +} + +object IpCidr { + + def tryParse(value: String): IpCidr = + parse(value).getOrElse( + throw new IllegalArgumentException( + s"'$value' is not a valid IP address or network in CIDR notation" + ) + ) + + def parse(value: String): Option[IpCidr] = + value.split("/", -1) match { + case Array(address) => + ofAddress(address).map { case (high, low, addressBits) => + IpCidr(high, low, addressBits, addressBits) + } + case Array(address, prefixLength) => + ofAddress(address).flatMap { case (high, low, addressBits) => + Try(prefixLength.trim.toInt).toOption + .filter(length => length >= 0 && length <= addressBits) + .map(length => + // zero out the host bits so that `contains` reduces to an equality check + IpCidr(high & mask64(length), low & mask64(length - 64), length, addressBits) + ) + } + case _ => None + } + + /** Decodes an IP literal into `(high, low, addressBits)`. */ + private def ofAddress(address: String): Option[(Long, Long, Int)] = { + val trimmed = address.trim + // must not do a DNS lookup, only IP literals are accepted + Option + .when(InetAddresses.isInetAddress(trimmed))(InetAddresses.forString(trimmed)) + // IPv4-mapped IPv6 addresses (e.g. ::ffff:192.0.2.1) are converted to their IPv4 address by + // InetAddresses.forString, so they match the same networks as the plain IPv4 address + .map { inetAddress => + val bytes = inetAddress.getAddress + val (high, low) = pack(bytes, 0, 0L, 0L) + (high, low, bytes.length * 8) + } + } + + /** Packs the big-endian address `bytes` left-aligned into a 128 bit `(high, low)` word. */ + @tailrec + private def pack(bytes: Array[Byte], index: Int, high: Long, low: Long): (Long, Long) = + if (index >= bytes.length) (high, low) + else { + val byte = bytes(index) & 0xffL + if (index < 8) pack(bytes, index + 1, high | (byte << (56 - 8 * index)), low) + else pack(bytes, index + 1, high, low | (byte << (56 - 8 * (index - 8)))) + } + + /** A big-endian mask keeping the leading `bits` bits, clamped to `[0, 64]`. */ + private[util] def mask64(bits: Int): Long = + if (bits <= 0) 0L + else if (bits >= 64) -1L + else -1L << (64 - bits) +} + +object IpCidrRateLimits { + + private val noOverride: String => Option[SpliceRateLimitConfig.Simple] = _ => None + + /** Returns a matcher for the given config, resolving a client IP to the limit of the most + * specific network it is contained in. The config is parsed and sorted once here so that the + * per-request path only has to parse the client IP and run bitwise comparisons. + */ + def matchClientIp( + config: PerAttributeRateLimitConfig + ): String => Option[SpliceRateLimitConfig.Simple] = { + val parsedNetworks = networks(config.attributeOverrides).toArray + if (parsedNetworks.isEmpty) noOverride + else + clientIp => + IpCidr.parse(clientIp).flatMap { ip => + parsedNetworks.collectFirst { case (network, limit) if network.contains(ip) => limit } + } + } + + def tryValidate(config: PerAttributeRateLimitConfig): Unit = + networks(config.attributeOverrides).discard + + private def networks( + overrides: Map[String, SpliceRateLimitConfig.Simple] + ): Seq[(IpCidr, SpliceRateLimitConfig.Simple)] = + overrides.toSeq + .map { case (cidr, limit) => IpCidr.tryParse(cidr) -> limit } + // most specific network first, so that it takes precedence over the networks containing it + .sortBy { case (network, _) => -network.prefixLength } +} diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala index 7936cb0e057..410259b5b8a 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/PackageVetting.scala @@ -85,6 +85,7 @@ class PackageVetting( PackageIdResolver.Package.TokenStandard.SpliceTokenStandardUtils, PackageIdResolver.Package.TokenStandard.SpliceUtilTokenStandardWallet, PackageIdResolver.Package.SpliceUtilBatchedMarkers, + PackageIdResolver.Package.SpliceApiRewardAssignmentV1, ).flatMap(pkg => DarResourcesUtil .getRequiredPackageVersions( diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala index c1885094dd8..f3caa6e106b 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiter.scala @@ -73,7 +73,7 @@ case class SpliceRateLimitMetrics( } -sealed trait SpliceRateLimitConfig { +trait SpliceRateLimitConfig { def enabled: Boolean @@ -93,14 +93,6 @@ object SpliceRateLimitConfig { sustainedWindowSeconds: Long = SpliceRateLimiter.DefaultSustainedWindowSeconds, ) extends SpliceRateLimitConfig - final case class WithPerClientIp( - enabled: Boolean = true, - ratePerSecond: Double, - sustainedRatePerSecond: Option[Double] = None, - sustainedWindowSeconds: Long = SpliceRateLimiter.DefaultSustainedWindowSeconds, - perClientIp: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig.Disabled, - ) extends SpliceRateLimitConfig - def apply( enabled: Boolean = true, ratePerSecond: Double, @@ -114,15 +106,15 @@ case class PerAttributeRateLimitConfig( enabled: Boolean = true, limit: SpliceRateLimitConfig.Simple = PerAttributeRateLimitConfig.DefaultLimit, maxAttributeValues: Long = 10000, -) { - - def rateLimitFor(overall: SpliceRateLimitConfig): SpliceRateLimitConfig.Simple = - limit.copy(enabled = enabled && limit.enabled && overall.enabled) -} + attributeOverrides: Map[String, SpliceRateLimitConfig.Simple] = Map.empty, +) object PerAttributeRateLimitConfig { + val DefaultLimit: SpliceRateLimitConfig.Simple = SpliceRateLimitConfig(ratePerSecond = 10) - val Disabled: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig(enabled = false) + + def disabled: PerAttributeRateLimitConfig = + PerAttributeRateLimitConfig(enabled = false) } object SpliceRateLimiter { @@ -152,30 +144,35 @@ class SpliceRateLimiter( extraLabels ++ Map("limiter" -> name, "limiter_type" -> limiterType) ) + private val rejectAll: Boolean = + config.enabled && + (config.ratePerSecond <= 0 || config.sustainedRatePerSecond.exists(_ <= 0)) + // The limiters are created with one second worth of permits already available private val limiter: Option[RateLimiter] = - Option.when(config.enabled)(BurstyRateLimiterFactory.create(config.ratePerSecond)) + Option.when(config.enabled && !rejectAll)( + BurstyRateLimiterFactory.create(config.ratePerSecond) + ) // enforces the sustained limit over the sustained window, while still allowing bursts within its budget. private val sustainedLimiter: Option[RateLimiter] = Option - .when(config.enabled)(config.sustainedRatePerSecond) + .when(config.enabled && !rejectAll)(config.sustainedRatePerSecond) .flatten .map( BurstyRateLimiterFactory .create(_, SpliceRateLimiter.sustainedWindow(config).toSeconds.toDouble) ) // lazy to ensure metrics get registered only if the limiter is actually used - private lazy val rateLimiter: Option[RateLimiter] = { + private lazy val reportedMaxLimit: Unit = if (reportMaxLimit) { - metrics - .recordMaxLimit(config.ratePerSecond)(metricsContext) + metrics.recordMaxLimit(config.ratePerSecond)(metricsContext) } - limiter - } def markRun(): Boolean = { if (config.enabled) { - val canRun = rateLimiter.forall(_.tryAcquire()) && sustainedLimiter.forall(_.tryAcquire()) + reportedMaxLimit + val canRun = + !rejectAll && limiter.forall(_.tryAcquire()) && sustainedLimiter.forall(_.tryAcquire()) if (canRun) { metrics.meter.mark()( metricsContext.merge(MetricsContext("result" -> "accepted")) @@ -206,14 +203,17 @@ class SpliceRateLimiter( class PerAttributeRateLimiter( name: String, attribute: String, - config: SpliceRateLimitConfig, - attributeConfig: PerAttributeRateLimitConfig, + config: PerAttributeRateLimitConfig, metrics: SpliceRateLimitMetrics, logger: TracedLogger, + attributeMatcherFactory: PerAttributeRateLimitConfig => String => Option[ + SpliceRateLimitConfig.Simple + ] = PerAttributeRateLimiter.exactMatch, ) { - private val perAttributeConfig = attributeConfig.rateLimitFor(config) - private val isEnabled = perAttributeConfig.enabled && perAttributeConfig.ratePerSecond > 0 + private val attributeMatcher: String => Option[SpliceRateLimitConfig.Simple] = + attributeMatcherFactory(config) + private val attributeLabel = Map("limiter_attribute" -> attribute) // evictions by size can happen for every single request (e.g. when a large number of distinct @@ -227,7 +227,7 @@ class PerAttributeRateLimiter( implicit val tc: TraceContext = TraceContext.empty val message = s"Rate limiter cache for $name (attribute '$attribute') exceeded its maximum size of " + - s"${attributeConfig.maxAttributeValues}; evicting the rate limiter for attribute value '$key'. " + + s"${config.maxAttributeValues}; evicting the rate limiter for attribute value '$key'. " + "Its rate limiting state is lost. Consider increasing max-attribute-values." val now = System.nanoTime() val last = lastSizeEvictionWarning.get() @@ -249,17 +249,25 @@ class PerAttributeRateLimiter( ]( Caffeine .newBuilder() - .maximumSize(attributeConfig.maxAttributeValues) + .maximumSize(config.maxAttributeValues) // Evict limiters that have not been used for a full sustained rate limiting window (the bucket // size of the interval rate limiter): after that time an idle limiter would have refilled its // budget anyway, so dropping it does not change the enforced rate. - .expireAfterAccess(SpliceRateLimiter.sustainedWindow(perAttributeConfig)) + // The longest window of the default limit and all overrides is used, so that no limiter is + // evicted before its own window has elapsed. + .expireAfterAccess( + (config.limit +: config.attributeOverrides.values.toSeq) + .map(SpliceRateLimiter.sustainedWindow) + .foldLeft(Duration.ZERO)((longest, window) => + if (window.compareTo(longest) > 0) window else longest + ) + ) .evictionListener(evictionListener), Some(new CacheMetrics(s"$name-$attribute-rate-limiter", metrics.otelFactory)), ) private lazy val reportedMaxLimit: Unit = - metrics.recordMaxLimit(perAttributeConfig.ratePerSecond)( + metrics.recordMaxLimit(config.limit.ratePerSecond)( MetricsContext( attributeLabel ++ Map( "limiter" -> name, @@ -269,7 +277,7 @@ class PerAttributeRateLimiter( ) def markRun(attributeValue: Option[String]): Boolean = - if (isEnabled) attributeValue match { + if (config.enabled) attributeValue match { case Some(value) => limiterFor(value).markRun() case None => metrics.recordUnknownAttributeNotLimited()( @@ -291,7 +299,7 @@ class PerAttributeRateLimiter( (_: String) => new SpliceRateLimiter( name, - perAttributeConfig, + attributeMatcher(attributeValue).getOrElse(config.limit), metrics, limiterType = SpliceRateLimiter.PerAttributeLimiterType, extraLabels = attributeLabel, @@ -304,4 +312,10 @@ class PerAttributeRateLimiter( object PerAttributeRateLimiter { private val EvictionWarningIntervalNanos: Long = TimeUnit.MINUTES.toNanos(1) + + val exactMatch: PerAttributeRateLimitConfig => String => Option[SpliceRateLimitConfig.Simple] = + config => attributeValue => config.attributeOverrides.get(attributeValue) + + val noOverrides: PerAttributeRateLimitConfig => String => Option[SpliceRateLimitConfig.Simple] = + _ => _ => None } diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceUtil.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceUtil.scala index f26ac0fc3d6..f40d90f8f6b 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceUtil.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SpliceUtil.scala @@ -347,6 +347,7 @@ object SpliceUtil { Optional.empty(), // nextScheduledSynchronizerUpgrade Optional.empty(), // voteCooldownTime Optional.empty(), // nextScheduledLogicalSynchronizerUpgrade + Optional.empty(), // svOperationsSwitchOverTimes ) def defaultAmuletConfig( @@ -399,6 +400,12 @@ object SpliceUtil { initialRewardConfig.toJava, // transferPreapprovalBaseDuration Optional.empty(), + // developmentFundManagerBlacklist + Optional.empty(), + // minDevelopmentFundMintingDelay + Optional.empty(), + // amuletSwitchOverTimes + Optional.empty(), ) def defaultAnsConfig( diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SwitchOverTimes.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SwitchOverTimes.scala new file mode 100644 index 00000000000..eec335d61f8 --- /dev/null +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/util/SwitchOverTimes.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.util + +import com.digitalasset.canton.data.CantonTimestamp +import com.digitalasset.canton.time.Clock +import java.time.Instant +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.DsoRules + +object SwitchOverTimes { + + private def shouldSwitchOver( + clock: Clock, + times: java.util.Optional[java.util.Map[String, Instant]], + key: String, + ): Boolean = { + val timeO = Option(times.orElse(java.util.Map.of()).get(key)) + timeO.fold(false)(time => clock.now >= CantonTimestamp.assertFromInstant(time)) + } + + private def shouldSwitchOver(clock: Clock, dsoRules: DsoRules, key: String): Boolean = + shouldSwitchOver(clock, dsoRules.config.svOperationsSwitchOverTimes, key) + + def omitFeaturedAppRightInChoiceContext(clock: Clock, dsoRules: DsoRules) = + shouldSwitchOver(clock, dsoRules, NoFeaturedAppChoiceContext) + + val NoFeaturedAppChoiceContext = "no-featured-app-choice-context" +} diff --git a/apps/common/src/test/java/org/lfdecentralizedtrust/splice/util/scalatesttags/SpliceDsoGovernance_0_1_29.java b/apps/common/src/test/java/org/lfdecentralizedtrust/splice/util/scalatesttags/SpliceDsoGovernance_0_1_29.java new file mode 100644 index 00000000000..fe719d6d446 --- /dev/null +++ b/apps/common/src/test/java/org/lfdecentralizedtrust/splice/util/scalatesttags/SpliceDsoGovernance_0_1_29.java @@ -0,0 +1,14 @@ +package org.lfdecentralizedtrust.splice.util.scalatesttags; + +import org.scalatest.TagAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// Don't run this test when testing against splice-dso-governance < 0.1.29 +@TagAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface SpliceDsoGovernance_0_1_29 {} diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala index da4306e8807..0af17a16a11 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/HttpRateLimiterTest.scala @@ -16,7 +16,7 @@ import org.apache.pekko.http.scaladsl.model.{ import org.apache.pekko.http.scaladsl.server.Directives.* import org.apache.pekko.http.scaladsl.server.Route import org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest -import org.lfdecentralizedtrust.splice.config.RateLimitersConfig +import org.lfdecentralizedtrust.splice.config.{PerClientIpRateLimitConfig, RateLimitersConfig} import org.lfdecentralizedtrust.splice.util.{ PerAttributeRateLimitConfig, SpliceRateLimitConfig, @@ -28,94 +28,117 @@ import java.net.InetAddress class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteTest { + import HttpRateLimiterTest.{IpVersion, IpVersions} + "clientIp" should { - "prefer X-Forwarded-For" in { - clientIp( - HttpRequest() - .withHeaders( - `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))), - `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), - ) - .withAttributes( - Map( - AttributeKeys.remoteAddress -> RemoteAddress(InetAddress.getByName("3.3.3.3")) + IpVersions.foreach { v => + s"prefer X-Forwarded-For (${v.name})" in { + clientIp( + HttpRequest() + .withHeaders( + `X-Forwarded-For`(remoteAddress(v.address(0))), + `X-Real-Ip`(remoteAddress(v.address(1))), ) - ) - ) should be(Some("1.1.1.1")) - } + .withAttributes( + Map( + AttributeKeys.remoteAddress -> remoteAddress(v.address(2)) + ) + ) + ) should be(Some(v.key(0))) + } - "fall back to X-Real-Ip" in { - clientIp( - HttpRequest().withHeaders(`X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2")))) - ) should be(Some("2.2.2.2")) - } + s"fall back to X-Real-Ip (${v.name})" in { + clientIp( + HttpRequest().withHeaders(`X-Real-Ip`(remoteAddress(v.address(1)))) + ) should be(Some(v.key(1))) + } - "ignore a non-IP value and fall back to the next header" in { - clientIp( - HttpRequest() - .withHeaders( - RawHeader("X-Forwarded-For", "evil.example.com"), - `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), + s"ignore a non-IP value and fall back to the next header (${v.name})" in { + clientIp( + HttpRequest() + .withHeaders( + RawHeader("X-Forwarded-For", "evil.example.com"), + `X-Real-Ip`(remoteAddress(v.address(1))), + ) + ) should be(Some(v.key(1))) + } + + s"use the first address of a comma separated header value (${v.name})" in { + clientIp( + HttpRequest().withHeaders( + RawHeader( + "X-Forwarded-For", + s"${v.address(0)}, ${v.address(1)}, ${v.address(2)}", + ) ) - ) should be(Some("2.2.2.2")) - } + ) should be(Some(v.key(0))) + } - "use the first address of a comma separated header value" in { - clientIp( - HttpRequest().withHeaders(RawHeader("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3")) - ) should be(Some("1.1.1.1")) - } + s"use the configured headers in order (${v.name})" in { + val request = HttpRequest().withHeaders( + RawHeader("X-Envoy-External-Address", v.address(3)), + `X-Forwarded-For`(remoteAddress(v.address(0))), + `X-Real-Ip`(remoteAddress(v.address(1))), + ) + clientIp( + request, + clientIpHeaders = Seq("x-envoy-external-address", "x-forwarded-for"), + ) should be(Some(v.key(3))) + clientIp( + request, + clientIpHeaders = Seq("x-real-ip", "x-envoy-external-address"), + ) should be(Some(v.key(1))) + } - "use the configured headers in order" in { - val request = HttpRequest().withHeaders( - RawHeader("X-Envoy-External-Address", "4.4.4.4"), - `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))), - `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), - ) - clientIp( - request, - clientIpHeaders = Seq("x-envoy-external-address", "x-forwarded-for"), - ) should be(Some("4.4.4.4")) - clientIp( - request, - clientIpHeaders = Seq("x-real-ip", "x-envoy-external-address"), - ) should be(Some("2.2.2.2")) - } + s"not use headers that are not configured (${v.name})" in { + clientIp( + HttpRequest().withHeaders( + `X-Forwarded-For`(remoteAddress(v.address(0))), + `X-Real-Ip`(remoteAddress(v.address(1))), + ), + clientIpHeaders = Seq("x-envoy-external-address"), + ) should be(None) + } - "not use headers that are not configured" in { - clientIp( - HttpRequest().withHeaders( - `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))), - `X-Real-Ip`(RemoteAddress(InetAddress.getByName("2.2.2.2"))), - ), - clientIpHeaders = Seq("x-envoy-external-address"), - ) should be(None) - } + s"match the configured headers case-insensitively (${v.name})" in { + clientIp( + HttpRequest().withHeaders(RawHeader("X-Envoy-External-Address", v.address(3))), + clientIpHeaders = Seq("X-Envoy-External-Address"), + ) should be(Some(v.key(3))) + } - "match the configured headers case-insensitively" in { - clientIp( - HttpRequest().withHeaders(RawHeader("X-Envoy-External-Address", "4.4.4.4")), - clientIpHeaders = Seq("X-Envoy-External-Address"), - ) should be(Some("4.4.4.4")) - } + s"not extract any IP when no headers are configured (${v.name})" in { + clientIp( + HttpRequest().withHeaders( + `X-Forwarded-For`(remoteAddress(v.address(0))) + ), + clientIpHeaders = Seq.empty, + ) should be(None) + } - "not extract any IP when no headers are configured" in { - clientIp( - HttpRequest().withHeaders( - `X-Forwarded-For`(RemoteAddress(InetAddress.getByName("1.1.1.1"))) - ), - clientIpHeaders = Seq.empty, - ) should be(None) - } + s"not use the remote address of the transport connection (${v.name})" in { + // the remote address is not exposed by the server, so it must not be relied upon + clientIp( + HttpRequest().withAttributes( + Map(AttributeKeys.remoteAddress -> remoteAddress(v.address(2))) + ) + ) should be(None) + } - "not use the remote address of the transport connection" in { - // the remote address is not exposed by the server, so it must not be relied upon - clientIp( - HttpRequest().withAttributes( - Map(AttributeKeys.remoteAddress -> RemoteAddress(InetAddress.getByName("3.3.3.3"))) - ) - ) should be(None) + s"apply the same grouping to all client IP sources (${v.name})" in { + val expected = Some(v.key(0)) + clientIp( + HttpRequest().withHeaders(RawHeader("X-Envoy-External-Address", v.address(0))), + clientIpHeaders = Seq("x-envoy-external-address"), + ) should be(expected) + clientIp( + HttpRequest().withHeaders(`X-Forwarded-For`(remoteAddress(v.address(0)))) + ) should be(expected) + clientIp( + HttpRequest().withHeaders(`X-Real-Ip`(remoteAddress(v.address(0)))) + ) should be(expected) + } } "return None if no IP can be determined" in { @@ -132,6 +155,11 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT clientIpOf("1.2.3.4") should be(Some("1.2.3.4")) } + "not group IPv4 clients of the same network" in { + // a whole IPv4 network is not assigned to a single client, so they are limited individually + clientIpOf("1.2.3.4") should not be clientIpOf("1.2.3.5") + } + "group IPv6 clients by their /64 prefix" in { // the lower 64 bits (the interface identifier) are freely chosen by the client clientIpOf("2001:db8:0:1:1:2:3:4") should be(Some("2001:db8:0:1:0:0:0:0/64")) @@ -159,47 +187,162 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT clientIpOf("::ffff:4.3.2.1") should not be clientIpOf("::ffff:1.2.3.4") } - "apply the same grouping to all client IP sources" in { - val expected = Some("2001:db8:0:1:0:0:0:0/64") - val address = RemoteAddress(InetAddress.getByName("2001:db8:0:1:1:2:3:4")) - clientIp( - HttpRequest().withHeaders(RawHeader("X-Envoy-External-Address", "2001:db8:0:1:1:2:3:4")), - clientIpHeaders = Seq("x-envoy-external-address"), - ) should be(expected) - clientIp( - HttpRequest().withHeaders(`X-Forwarded-For`(address)) - ) should be(expected) - clientIp( - HttpRequest().withHeaders(`X-Real-Ip`(address)) - ) should be(expected) + "never use the same key for an IPv4 and an IPv6 client" in { + clientIpOf("1.2.3.4") should not be clientIpOf("2001:db8:0:1::1") } } "the http rate limiter" should { - "reject requests of a client IP over the global per client IP limit" in { - // the global per client IP limiter is enabled by default - withRoutes( - globalPerClientIp = perClientIp(1) - )("testOperation") { routes => - val route = routes("testOperation") - val results = (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) - // 1 request per second per client IP, with 1 permit available from the creation of the - // limiter plus guava's deferred payment for the next one => the rest of the burst is rejected - results.count(_ == StatusCodes.OK) should be(2) - results.count(_ == StatusCodes.TooManyRequests) should be(18) + IpVersions.foreach { v => + s"reject requests of a client IP over the global per client IP limit (${v.name})" in { + // the global per client IP limiter is enabled by default + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + val results = (1 to 20).map(_ => call(route, ip = Some(v.address(0)))) + // 1 request per second per client IP, with 1 permit available from the creation of the + // limiter plus guava's deferred payment for the next one => the rest of the burst is rejected + results.count(_ == StatusCodes.OK) should be(2) + results.count(_ == StatusCodes.TooManyRequests) should be(18) + } } - } - "not reject requests of other client IPs" in { - withRoutes( - globalPerClientIp = perClientIp(1) - )("testOperation") { routes => - val route = routes("testOperation") - (1 to 20) - .map(_ => call(route, ip = Some("1.1.1.1"))) - .count(_ == StatusCodes.TooManyRequests) should be > 0 - call(route, ip = Some("2.2.2.2")) should be(StatusCodes.OK) + s"not reject requests of other client IPs (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + (1 to 20) + .map(_ => call(route, ip = Some(v.address(0)))) + .count(_ == StatusCodes.TooManyRequests) should be > 0 + call(route, ip = Some(v.address(1))) should be(StatusCodes.OK) + } + } + + s"not apply the per client IP limiter if no client IP is known (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => + val route = routes("testOperation") + (1 to 20).map(_ => call(route, ip = None)) should contain only StatusCodes.OK + val results = (1 to 20).map(_ => call(route, ip = Some(v.address(0)))) + results.count(_ == StatusCodes.OK) should be(2) + results.count(_ == StatusCodes.TooManyRequests) should be(18) + } + } + + s"not apply the per client IP limiters if no client IP headers are configured (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp(1), + perClientIpOverrides = Map("testOperation" -> perClientIp(1)), + clientIpHeaders = Seq.empty, + )("testOperation") { fixture => + val route = fixture("testOperation") + (1 to 20).map(_ => + call(route, ip = Some(v.address(0))) + ) should contain only StatusCodes.OK + forEvery(Seq("testOperation", HttpRateLimiter.GlobalLimiter)) { limiter => + fixture.requestsRejectedBy( + limiter, + SpliceRateLimiter.PerAttributeLimiterType, + ) should be(0L) + } + } + } + + s"apply the global per client IP limiter across operations (${v.name})" in { + // the same client IP is limited regardless of the operation + withRoutes( + globalPerClientIp = perClientIp(1) + )("operationA", "operationB") { routes => + (1 to 20) + .map(_ => call(routes("operationA"), ip = Some(v.address(0)))) + .count(_ == StatusCodes.OK) should be > 0 + call(routes("operationB"), ip = Some(v.address(0))) should be( + StatusCodes.TooManyRequests + ) + } + } + + s"apply the global overall limiter across operations (${v.name})" in { + withRoutes( + global = SpliceRateLimitConfig(ratePerSecond = 1), + globalPerClientIp = PerAttributeRateLimitConfig.disabled, + )("operationA", "operationB") { routes => + // exhaust the global budget via operationA + (1 to 20).map(_ => call(routes("operationA"), ip = Some(v.address(0)))) + // the global limiter ignores the operation and the client IP, so operationB is rejected too + call(routes("operationB"), ip = Some(v.address(1))) should be( + StatusCodes.TooManyRequests + ) + } + } + + s"not apply the per operation client IP limiter by default (${v.name})" in { + // no per client IP limiting configured for operations => requests from a single IP are only + // bounded by the (high) overall limiters + withRoutes()("testOperation") { routes => + val route = routes("testOperation") + (1 to 20).map(_ => + call(route, ip = Some(v.address(0))) + ) should contain only StatusCodes.OK + } + } + + s"apply the per operation client IP limiter when enabled for an operation (${v.name})" in { + withRoutes( + perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)) + )("limitedOperation", "otherOperation") { routes => + val results = + (1 to 20).map(_ => call(routes("limitedOperation"), ip = Some(v.address(0)))) + results.count(_ == StatusCodes.OK) should be(2) + // a different operation is not affected by the per operation client IP limiter + call(routes("otherOperation"), ip = Some(v.address(0))) should be(StatusCodes.OK) + } + } + + s"apply the per operation overall limiter (${v.name})" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> SpliceRateLimitConfig(ratePerSecond = 1)) + )("limitedOperation", "otherOperation") { routes => + val results = + (1 to 20).map(_ => call(routes("limitedOperation"), ip = Some(v.address(0)))) + results.count(_ == StatusCodes.TooManyRequests) should be > 0 + // a different operation uses a separate overall limiter and is not affected + call(routes("otherOperation"), ip = Some(v.address(0))) should be(StatusCodes.OK) + } + } + + s"use separate per operation limiters for equally named operations of different services (${v.name})" in { + val rateLimiter = new HttpRateLimiter( + RateLimitersConfig( + default = withPerClientIp( + SpliceRateLimitConfig(ratePerSecond = 1), + PerAttributeRateLimitConfig.disabled, + ), + rateLimiters = Map.empty, + global = withPerClientIp( + SpliceRateLimitConfig(ratePerSecond = 1000), + PerAttributeRateLimitConfig.disabled, + ), + ), + new InMemoryMetricsFactory(), + loggerFactory.getTracedLogger(classOf[HttpRateLimiterTest]), + ) + try { + val routeV1 = + rateLimiter.withRateLimit("serviceV1")("sharedOperation")(complete(StatusCodes.OK)) + val routeV2 = + rateLimiter.withRateLimit("serviceV2")("sharedOperation")(complete(StatusCodes.OK)) + (1 to 20) + .map(_ => call(routeV1, ip = Some(v.address(0)))) + .count(_ == StatusCodes.TooManyRequests) should be > 0 + call(routeV2, ip = Some(v.address(0))) should be(StatusCodes.OK) + } finally { + rateLimiter.close() + } } } @@ -221,118 +364,27 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT } } - "not apply the per client IP limiter if no client IP is known" in { + "limit an IPv4 client and its IPv4-mapped IPv6 form with the same limiter" in { withRoutes( globalPerClientIp = perClientIp(1) )("testOperation") { routes => val route = routes("testOperation") - (1 to 20).map(_ => call(route, ip = None)) should contain only StatusCodes.OK - val results = (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) - results.count(_ == StatusCodes.OK) should be(2) - results.count(_ == StatusCodes.TooManyRequests) should be(18) - } - } - - "not apply the per client IP limiters if no client IP headers are configured" in { - withRoutes( - globalPerClientIp = perClientIp(1), - perClientIpOverrides = Map("testOperation" -> perClientIp(1)), - clientIpHeaders = Seq.empty, - )("testOperation") { fixture => - val route = fixture("testOperation") - (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) should contain only StatusCodes.OK - forEvery(Seq("testOperation", HttpRateLimiter.GlobalLimiter)) { limiter => - fixture.requestsRejectedBy( - limiter, - SpliceRateLimiter.PerAttributeLimiterType, - ) should be(0L) - } - } - } - - "apply the global per client IP limiter across operations" in { - // the same client IP is limited regardless of the operation - withRoutes( - globalPerClientIp = perClientIp(1) - )("operationA", "operationB") { routes => (1 to 20) - .map(_ => call(routes("operationA"), ip = Some("1.1.1.1"))) - .count(_ == StatusCodes.OK) should be > 0 - call(routes("operationB"), ip = Some("1.1.1.1")) should be(StatusCodes.TooManyRequests) + .map(_ => call(route, ip = Some("1.2.3.4"))) + .count(_ == StatusCodes.TooManyRequests) should be > 0 + call(route, ip = Some("::ffff:1.2.3.4")) should be(StatusCodes.TooManyRequests) } } - "apply the global overall limiter across operations" in { + "limit IPv4 and IPv6 clients independently" in { withRoutes( - global = SpliceRateLimitConfig(ratePerSecond = 1), - globalPerClientIp = PerAttributeRateLimitConfig.Disabled, - )("operationA", "operationB") { routes => - // exhaust the global budget via operationA - (1 to 20).map(_ => call(routes("operationA"), ip = Some("1.1.1.1"))) - // the global limiter ignores the operation and the client IP, so operationB is rejected too - call(routes("operationB"), ip = Some("2.2.2.2")) should be(StatusCodes.TooManyRequests) - } - } - - "not apply the per operation client IP limiter by default" in { - // no per client IP limiting configured for operations => requests from a single IP are only - // bounded by the (high) overall limiters - withRoutes()("testOperation") { routes => + globalPerClientIp = perClientIp(1) + )("testOperation") { routes => val route = routes("testOperation") - (1 to 20).map(_ => call(route, ip = Some("1.1.1.1"))) should contain only StatusCodes.OK - } - } - - "apply the per operation client IP limiter when enabled for an operation" in { - withRoutes( - perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)) - )("limitedOperation", "otherOperation") { routes => - val results = - (1 to 20).map(_ => call(routes("limitedOperation"), ip = Some("1.1.1.1"))) - results.count(_ == StatusCodes.OK) should be(2) - // a different operation is not affected by the per operation client IP limiter - call(routes("otherOperation"), ip = Some("1.1.1.1")) should be(StatusCodes.OK) - } - } - - "apply the per operation overall limiter" in { - withRoutes( - rateLimiters = Map("limitedOperation" -> SpliceRateLimitConfig(ratePerSecond = 1)) - )("limitedOperation", "otherOperation") { routes => - val results = (1 to 20).map(_ => call(routes("limitedOperation"), ip = Some("1.1.1.1"))) - results.count(_ == StatusCodes.TooManyRequests) should be > 0 - // a different operation uses a separate overall limiter and is not affected - call(routes("otherOperation"), ip = Some("1.1.1.1")) should be(StatusCodes.OK) - } - } - - "use separate per operation limiters for equally named operations of different services" in { - val rateLimiter = new HttpRateLimiter( - RateLimitersConfig( - default = withPerClientIp( - SpliceRateLimitConfig(ratePerSecond = 1), - PerAttributeRateLimitConfig.Disabled, - ), - rateLimiters = Map.empty, - global = withPerClientIp( - SpliceRateLimitConfig(ratePerSecond = 1000), - PerAttributeRateLimitConfig.Disabled, - ), - ), - new InMemoryMetricsFactory(), - loggerFactory.getTracedLogger(classOf[HttpRateLimiterTest]), - ) - try { - val routeV1 = - rateLimiter.withRateLimit("serviceV1")("sharedOperation")(complete(StatusCodes.OK)) - val routeV2 = - rateLimiter.withRateLimit("serviceV2")("sharedOperation")(complete(StatusCodes.OK)) (1 to 20) - .map(_ => call(routeV1, ip = Some("1.1.1.1"))) + .map(_ => call(route, ip = Some("1.2.3.4"))) .count(_ == StatusCodes.TooManyRequests) should be > 0 - call(routeV2, ip = Some("1.1.1.1")) should be(StatusCodes.OK) - } finally { - rateLimiter.close() + call(route, ip = Some("2001:db8:0:1::1")) should be(StatusCodes.OK) } } } @@ -351,8 +403,8 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT val Overall = SpliceRateLimiter.GlobalLimiterType // Sends a burst of requests from a single client IP and returns how many were accepted. - def burst(fixture: HttpRateLimiterTest.Fixture, operation: String): Long = { - val results = (1 to Burst).map(_ => call(fixture(operation), ip = Some("1.1.1.1"))) + def burst(fixture: HttpRateLimiterTest.Fixture, operation: String, ip: String): Long = { + val results = (1 to Burst).map(_ => call(fixture(operation), ip = Some(ip))) results.count(_ == StatusCodes.TooManyRequests) should be > 0 results.count(_ == StatusCodes.OK).toLong } @@ -368,74 +420,251 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT } } - "apply the per operation client IP limiter before the overall limiters" in { - withRoutes( - rateLimiters = Map("limitedOperation" -> Downstream), - global = Downstream, - perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)), - )("limitedOperation") { fixture => - val accepted = burst(fixture, "limitedOperation") - onlySawAcceptedRequests(fixture, accepted)( - "limitedOperation" -> Overall, - HttpRateLimiter.GlobalLimiter -> Overall, - ) + IpVersions.foreach { v => + s"apply the per operation client IP limiter before the overall limiters (${v.name})" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> Downstream), + global = Downstream, + perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)), + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation", v.address(0)) + onlySawAcceptedRequests(fixture, accepted)( + "limitedOperation" -> Overall, + HttpRateLimiter.GlobalLimiter -> Overall, + ) + } + } + + s"apply the global per client IP limiter before the overall limiters (${v.name})" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> Downstream), + global = Downstream, + globalPerClientIp = perClientIp(1), + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation", v.address(0)) + onlySawAcceptedRequests(fixture, accepted)( + "limitedOperation" -> Overall, + HttpRateLimiter.GlobalLimiter -> Overall, + ) + } + } + + s"apply the per operation client IP limiter before the global per client IP limiter (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp(1000), + perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)), + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation", v.address(0)) + onlySawAcceptedRequests(fixture, accepted)( + HttpRateLimiter.GlobalLimiter -> PerAttribute + ) + } + } + + s"apply the per operation overall limiter before the global overall limiter (${v.name})" in { + withRoutes( + rateLimiters = Map("limitedOperation" -> Rejecting), + global = Downstream, + )("limitedOperation") { fixture => + val accepted = burst(fixture, "limitedOperation", v.address(0)) + onlySawAcceptedRequests(fixture, accepted)( + HttpRateLimiter.GlobalLimiter -> Overall + ) + } + } + + s"still reject requests that pass the per client IP limiters but exceed an overall limit (${v.name})" in { + withRoutes( + global = SpliceRateLimitConfig(ratePerSecond = 2), + globalPerClientIp = perClientIp(1000), + perClientIpOverrides = Map("testOperation" -> perClientIp(1000)), + )("testOperation") { fixture => + // every request is below both per client IP limits, but the overall global limit applies + val results = + v.distinctClients(Burst).map(ip => call(fixture("testOperation"), ip = Some(ip))) + results.count(_ == StatusCodes.OK) should be < Burst + results.count(_ == StatusCodes.TooManyRequests) should be > 0 + } } } + } - "apply the global per client IP limiter before the overall limiters" in { - withRoutes( - rateLimiters = Map("limitedOperation" -> Downstream), - global = Downstream, - globalPerClientIp = perClientIp(1), - )("limitedOperation") { fixture => - val accepted = burst(fixture, "limitedOperation") - onlySawAcceptedRequests(fixture, accepted)( - "limitedOperation" -> Overall, - HttpRateLimiter.GlobalLimiter -> Overall, - ) + "the per client IP CIDR overrides" should { + + IpVersions.foreach { v => + s"apply the custom limit to clients of a matching network (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp( + 1, + cidrOverrides = Map(v.network -> SpliceRateLimitConfig(ratePerSecond = 5)), + ) + )("testOperation") { routes => + val route = routes("testOperation") + // 5 permits (plus guava's deferred payment) for a client of the overridden network + (1 to 20) + .map(_ => call(route, ip = Some(v.insideA))) + .count(_ == StatusCodes.OK) should be(6) + // every client of the network gets its own limiter + (1 to 20) + .map(_ => call(route, ip = Some(v.insideB))) + .count(_ == StatusCodes.OK) should be(6) + // clients outside of the network use the default per client IP limit + (1 to 20) + .map(_ => call(route, ip = Some(v.outside))) + .count(_ == StatusCodes.OK) should be(2) + } + } + + s"exempt clients of a network with a disabled override (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp( + 1, + cidrOverrides = + Map(v.network -> SpliceRateLimitConfig(enabled = false, ratePerSecond = 0)), + ) + )("testOperation") { routes => + val route = routes("testOperation") + (1 to 20).map(_ => call(route, ip = Some(v.insideA))) should contain only StatusCodes.OK + (1 to 20) + .map(_ => call(route, ip = Some(v.outside))) + .count(_ == StatusCodes.TooManyRequests) should be > 0 + } + } + + s"block clients of a network whose override rate is zero (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map(v.network -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + )("testOperation") { routes => + val route = routes("testOperation") + (1 to 20).map(_ => + call(route, ip = Some(v.insideA)) + ) should contain only StatusCodes.TooManyRequests + call(route, ip = Some(v.outside)) should be(StatusCodes.OK) + } + } + + s"apply the overrides of the per operation limiter (${v.name})" in { + withRoutes( + perClientIpOverrides = Map( + "limitedOperation" -> perClientIp( + 1, + cidrOverrides = Map(v.network -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + ) + )("limitedOperation", "otherOperation") { routes => + call(routes("limitedOperation"), ip = Some(v.insideA)) should be( + StatusCodes.TooManyRequests + ) + // a different operation is not affected + call(routes("otherOperation"), ip = Some(v.insideA)) should be(StatusCodes.OK) + } + } + + s"use the most specific of several matching networks (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map( + v.network -> SpliceRateLimitConfig(ratePerSecond = 1000), + v.hostOf(v.insideA) -> SpliceRateLimitConfig(ratePerSecond = 0), + ), + ) + )("testOperation") { routes => + val route = routes("testOperation") + call(route, ip = Some(v.insideA)) should be(StatusCodes.TooManyRequests) + // covered by the wider network only + call(route, ip = Some(v.insideB)) should be(StatusCodes.OK) + } + } + + s"not apply an override to clients of the other IP version (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map(v.network -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + )("testOperation") { routes => + val route = routes("testOperation") + call(route, ip = Some(v.insideA)) should be(StatusCodes.TooManyRequests) + // a client of the other IP version is never contained in the configured network + call(route, ip = Some(other(v).insideA)) should be(StatusCodes.OK) + } + } + + s"apply an override matching every address of the IP version (${v.name})" in { + withRoutes( + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map(v.anyNetwork -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + )("testOperation") { routes => + val route = routes("testOperation") + call(route, ip = Some(v.address(0))) should be(StatusCodes.TooManyRequests) + call(route, ip = Some(v.outside)) should be(StatusCodes.TooManyRequests) + // a zero length prefix of one IP version does not match the other one + call(route, ip = Some(other(v).address(0))) should be(StatusCodes.OK) + } } } - "apply the per operation client IP limiter before the global per client IP limiter" in { + "match an IPv4-mapped IPv6 client against the plain IPv4 network" in { withRoutes( - globalPerClientIp = perClientIp(1000), - perClientIpOverrides = Map("limitedOperation" -> perClientIp(1)), - )("limitedOperation") { fixture => - val accepted = burst(fixture, "limitedOperation") - onlySawAcceptedRequests(fixture, accepted)( - HttpRateLimiter.GlobalLimiter -> PerAttribute + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map("10.0.0.0/8" -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + )("testOperation") { routes => + call(routes("testOperation"), ip = Some("::ffff:10.1.2.3")) should be( + StatusCodes.TooManyRequests ) } } - "apply the per operation overall limiter before the global overall limiter" in { + "apply an IPv6 override to all clients of the covered /64 networks" in { withRoutes( - rateLimiters = Map("limitedOperation" -> Rejecting), - global = Downstream, - )("limitedOperation") { fixture => - val accepted = burst(fixture, "limitedOperation") - onlySawAcceptedRequests(fixture, accepted)( - HttpRateLimiter.GlobalLimiter -> Overall + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map("2001:db8:a::/48" -> SpliceRateLimitConfig(ratePerSecond = 0)), ) + )("testOperation") { routes => + val route = routes("testOperation") + forEvery(Seq("2001:db8:a::1", "2001:db8:a:1::1", "2001:db8:a:ffff:1:2:3:4")) { ip => + call(route, ip = Some(ip)) should be(StatusCodes.TooManyRequests) + } + call(route, ip = Some("2001:db8:b:1::1")) should be(StatusCodes.OK) } } - "still reject requests that pass the per client IP limiters but exceed an overall limit" in { + "not apply an IPv6 override that is more specific than the /64 rate limiting key" in { + // clients are grouped into /64s, so an override narrower than that can never match withRoutes( - global = SpliceRateLimitConfig(ratePerSecond = 2), - globalPerClientIp = perClientIp(1000), - perClientIpOverrides = Map("testOperation" -> perClientIp(1000)), - )("testOperation") { fixture => - // every request is below both per client IP limits, but the overall global limit applies - val results = (1 to Burst).map(i => call(fixture("testOperation"), ip = Some(s"1.1.1.$i"))) - results.count(_ == StatusCodes.OK) should be < Burst - results.count(_ == StatusCodes.TooManyRequests) should be > 0 + globalPerClientIp = perClientIp( + 1000, + cidrOverrides = Map("2001:db8:a:1:2::/80" -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + )("testOperation") { routes => + call(routes("testOperation"), ip = Some("2001:db8:a:1:2::1")) should be(StatusCodes.OK) } } } - private def perClientIp(ratePerSecond: Double): PerAttributeRateLimitConfig = - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = ratePerSecond)) + private def other(version: IpVersion): IpVersion = + if (version == HttpRateLimiterTest.Ipv4) HttpRateLimiterTest.Ipv6 else HttpRateLimiterTest.Ipv4 + + private def perClientIp( + ratePerSecond: Double, + cidrOverrides: Map[String, SpliceRateLimitConfig.Simple] = Map.empty, + ): PerAttributeRateLimitConfig = + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = ratePerSecond), + attributeOverrides = cidrOverrides, + ) + + private def remoteAddress(ip: String): RemoteAddress = + RemoteAddress(InetAddress.getByName(ip)) private def clientIp( request: HttpRequest, @@ -457,7 +686,7 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT private def call(route: Route, ip: Option[String]): StatusCode = { val request = ip match { case Some(value) => - Get("/") ~> addHeader(`X-Forwarded-For`(RemoteAddress(InetAddress.getByName(value)))) + Get("/") ~> addHeader(`X-Forwarded-For`(remoteAddress(value))) case None => Get("/") } request ~> route ~> check(status) @@ -468,23 +697,23 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT default: SpliceRateLimitConfig = SpliceRateLimitConfig(ratePerSecond = 1000), rateLimiters: Map[String, SpliceRateLimitConfig] = Map.empty, global: SpliceRateLimitConfig = SpliceRateLimitConfig(ratePerSecond = 1000), - globalPerClientIp: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig.Disabled, + globalPerClientIp: PerAttributeRateLimitConfig = PerAttributeRateLimitConfig.disabled, perClientIpOverrides: Map[String, PerAttributeRateLimitConfig] = Map.empty, clientIpHeaders: Seq[String] = RateLimitersConfig.DefaultClientIpHeaders, )(operations: String*)(f: HttpRateLimiterTest.Fixture => A): A = { // Any operation with a per client IP override needs its own overall limiter entry so that the // embedded per client IP limiter is used instead of the `default` one. - val perOperationConfigs: Map[String, SpliceRateLimitConfig.WithPerClientIp] = + val perOperationConfigs: Map[String, PerClientIpRateLimitConfig] = (rateLimiters.keySet ++ perClientIpOverrides.keySet).map { operation => operation -> withPerClientIp( rateLimiters.getOrElse(operation, default), - perClientIpOverrides.getOrElse(operation, PerAttributeRateLimitConfig.Disabled), + perClientIpOverrides.getOrElse(operation, PerAttributeRateLimitConfig.disabled), ) }.toMap val metricsFactory = new InMemoryMetricsFactory() val rateLimiter = new HttpRateLimiter( RateLimitersConfig( - default = withPerClientIp(default, PerAttributeRateLimitConfig.Disabled), + default = withPerClientIp(default, PerAttributeRateLimitConfig.disabled), rateLimiters = perOperationConfigs, global = withPerClientIp(global, globalPerClientIp), clientIpHeaders = clientIpHeaders, @@ -507,8 +736,8 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT private def withPerClientIp( overall: SpliceRateLimitConfig, perClientIp: PerAttributeRateLimitConfig, - ): SpliceRateLimitConfig.WithPerClientIp = - SpliceRateLimitConfig.WithPerClientIp( + ): PerClientIpRateLimitConfig = + PerClientIpRateLimitConfig( enabled = overall.enabled, ratePerSecond = overall.ratePerSecond, sustainedRatePerSecond = overall.sustainedRatePerSecond, @@ -520,6 +749,64 @@ class HttpRateLimiterTest extends AnyWordSpec with BaseTest with ScalatestRouteT object HttpRateLimiterTest { private val NoClientIp = "" + private[http] final case class IpVersion( + name: String, + addresses: Seq[String], + keys: Seq[String], + network: String, + insideA: String, + insideB: String, + outside: String, + anyNetwork: String, + hostNetwork: String => String, + clientTemplate: Int => String, + ) { + def address(index: Int): String = addresses(index) + + def key(index: Int): String = keys(index) + + def hostOf(address: String): String = hostNetwork(address) + + /** `n` distinct clients, each with its own rate limiting key. */ + def distinctClients(n: Int): Seq[String] = (1 to n).map(clientTemplate) + } + + private[http] val Ipv4: IpVersion = IpVersion( + name = "IPv4", + addresses = Seq("1.1.1.1", "2.2.2.2", "3.3.3.3", "4.4.4.4"), + keys = Seq("1.1.1.1", "2.2.2.2", "3.3.3.3", "4.4.4.4"), + network = "10.0.0.0/8", + insideA = "10.1.2.3", + insideB = "10.4.5.6", + outside = "11.1.2.3", + anyNetwork = "0.0.0.0/0", + // a single IPv4 host is rate limited by its full address + hostNetwork = address => s"$address/32", + clientTemplate = i => s"1.1.1.$i", + ) + + private[http] val Ipv6: IpVersion = IpVersion( + name = "IPv6", + addresses = Seq("2001:db8:1::1", "2001:db8:2::1", "2001:db8:3::1", "2001:db8:4::1"), + keys = Seq( + "2001:db8:1:0:0:0:0:0/64", + "2001:db8:2:0:0:0:0:0/64", + "2001:db8:3:0:0:0:0:0/64", + "2001:db8:4:0:0:0:0:0/64", + ), + network = "2001:db8:a::/48", + insideA = "2001:db8:a:1::1", + insideB = "2001:db8:a:2::1", + outside = "2001:db8:b:1::1", + anyNetwork = "::/0", + // IPv6 clients are grouped by /64, so that is the most specific network that can match one + hostNetwork = address => s"$address/64", + // distinct /64s, as clients of the same /64 share a limiter + clientTemplate = i => s"2001:db8:c:$i::1", + ) + + private[http] val IpVersions: Seq[IpVersion] = Seq(Ipv4, Ipv6) + /** The routes of the rate limited operations together with the metrics recorded by their rate * limiters. Requests are only recorded by the limiters they actually reach, which is what allows * asserting on the order in which the limiters are applied. diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/IpCidrRateLimitsTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/IpCidrRateLimitsTest.scala new file mode 100644 index 00000000000..ea8b231a189 --- /dev/null +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/IpCidrRateLimitsTest.scala @@ -0,0 +1,240 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.util + +import com.digitalasset.canton.BaseTest +import org.scalatest.wordspec.AnyWordSpecLike + +class IpCidrRateLimitsTest extends BaseTest with AnyWordSpecLike { + + import IpCidrRateLimitsTest.{IpVersion, IpVersions} + + private val networkLimit = SpliceRateLimitConfig(ratePerSecond = 5) + private val hostLimit = SpliceRateLimitConfig(ratePerSecond = 50) + + "the IP CIDR rate limit overrides" should { + + IpVersions.foreach { v => + s"match addresses within the configured network (${v.name})" in { + val overrides = perClientIp(v.network -> networkLimit) + + forEvery(v.inside)(ip => limitFor(overrides, ip) should be(Some(networkLimit))) + forEvery(v.outside)(ip => limitFor(overrides, ip) should be(empty)) + } + + s"match a bare IP address as a single host (${v.name})" in { + val overrides = perClientIp(v.host -> hostLimit) + + limitFor(overrides, v.host) should be(Some(hostLimit)) + limitFor(overrides, v.otherHost) should be(empty) + } + + s"use the most specific match (${v.name})" in { + val overrides = perClientIp(v.network -> networkLimit, v.host -> hostLimit) + + limitFor(overrides, v.host) should be(Some(hostLimit)) + limitFor(overrides, v.otherHost) should be(Some(networkLimit)) + } + + s"match everything for a zero length prefix (${v.name})" in { + val overrides = perClientIp(v.anyNetwork -> networkLimit) + + forEvery(v.inside ++ v.outside)(ip => limitFor(overrides, ip) should be(Some(networkLimit))) + // does not apply to the other IP version + forEvery(other(v).inside)(ip => limitFor(overrides, ip) should be(empty)) + } + + s"not match a client of the other IP version (${v.name})" in { + val overrides = perClientIp(v.network -> networkLimit) + + forEvery(other(v).inside ++ other(v).outside)(ip => + limitFor(overrides, ip) should be(empty) + ) + } + + s"not match values that are not IP addresses (${v.name})" in { + val overrides = perClientIp(v.network -> networkLimit) + + limitFor(overrides, "not-an-ip") should be(empty) + limitFor(overrides, "") should be(empty) + // must not do a DNS lookup + limitFor(overrides, "localhost") should be(empty) + } + + s"return a reusable matcher for a config (${v.name})" in { + val matcher = IpCidrRateLimits.matchClientIp(perClientIp(v.network -> networkLimit)) + + forEvery(v.inside)(ip => matcher(ip) should be(Some(networkLimit))) + forEvery(v.outside)(ip => matcher(ip) should be(empty)) + } + + s"reject invalid configurations (${v.name})" in { + forAll(v.invalidCidrs) { cidr => + val config = perClientIp(cidr -> networkLimit) + a[IllegalArgumentException] should be thrownBy IpCidrRateLimits.tryValidate(config) + a[IllegalArgumentException] should be thrownBy IpCidrRateLimits + .matchClientIp(config)(v.host) + } + } + + s"normalize the configured network (${v.name})" in { + forEvery(v.normalizations) { case (configured, normalized) => + IpCidr.tryParse(configured).toString should be(normalized) + } + } + } + + "not match a client network that is wider than the configured one" in { + val overrides = perClientIp("2001:db8:0:0:1::/80" -> networkLimit) + + // the /64 the client is grouped into is not fully covered by the configured /80 + limitFor(overrides, "2001:db8:0:0:0:0:0:0/64") should be(empty) + } + + "match IPv4-mapped IPv6 addresses like the plain IPv4 address" in { + val overrides = perClientIp("192.0.2.0/24" -> networkLimit) + + limitFor(overrides, "::ffff:192.0.2.1") should be(Some(networkLimit)) + limitFor(overrides, "::ffff:192.0.3.1") should be(empty) + } + + "treat an IPv4-mapped IPv6 network as the plain IPv4 network" in { + // ::ffff:a.b.c.d is decoded as the IPv4 address, so the prefix length is an IPv4 one + val overrides = perClientIp("::ffff:192.0.2.0/24" -> networkLimit) + + limitFor(overrides, "192.0.2.1") should be(Some(networkLimit)) + limitFor(overrides, "::ffff:192.0.2.1") should be(Some(networkLimit)) + limitFor(overrides, "192.0.3.1") should be(empty) + // an IPv6 prefix length is therefore out of range for a mapped address + a[IllegalArgumentException] should be thrownBy IpCidr.tryParse("::ffff:192.0.2.0/120") + } + + "match IPv6 prefixes across the 64 bit word boundary" in { + // /64 is exactly the boundary between the two words the address is stored in + val at64 = perClientIp("2001:db8:0:1::/64" -> networkLimit) + limitFor(at64, "2001:db8:0:1:ffff:ffff:ffff:ffff") should be(Some(networkLimit)) + limitFor(at64, "2001:db8:0:2::1") should be(empty) + + // a prefix beyond 64 bits must also discriminate on the low word + val beyond64 = perClientIp("2001:db8:0:1:2:3::/96" -> networkLimit) + limitFor(beyond64, "2001:db8:0:1:2:3:4:5") should be(Some(networkLimit)) + limitFor(beyond64, "2001:db8:0:1:2:4:0:0") should be(empty) + + // a full length /128 only matches the single host + val host = perClientIp("2001:db8:0:1:2:3:4:5" -> hostLimit) + limitFor(host, "2001:db8:0:1:2:3:4:5") should be(Some(hostLimit)) + limitFor(host, "2001:db8:0:1:2:3:4:6") should be(empty) + } + + "match IPv4 prefixes that are not on a byte boundary" in { + val overrides = perClientIp("10.1.2.0/23" -> networkLimit) + + limitFor(overrides, "10.1.2.255") should be(Some(networkLimit)) + limitFor(overrides, "10.1.3.255") should be(Some(networkLimit)) + limitFor(overrides, "10.1.4.1") should be(empty) + } + + "not match anything without overrides" in { + forEvery(IpVersions.flatMap(v => v.inside ++ v.outside)) { ip => + limitFor(PerAttributeRateLimitConfig(), ip) should be(empty) + } + } + } + + private def other(version: IpVersion): IpVersion = + if (version == IpCidrRateLimitsTest.Ipv4) IpCidrRateLimitsTest.Ipv6 + else IpCidrRateLimitsTest.Ipv4 + + private def perClientIp( + overrides: (String, SpliceRateLimitConfig.Simple)* + ): PerAttributeRateLimitConfig = + PerAttributeRateLimitConfig(attributeOverrides = overrides.toMap) + + private def limitFor( + config: PerAttributeRateLimitConfig, + clientIp: String, + ): Option[SpliceRateLimitConfig.Simple] = + IpCidrRateLimits.matchClientIp(config)(clientIp) +} + +object IpCidrRateLimitsTest { + + /** The sample networks and addresses of a single IP version, so that every CIDR override test can + * be run for both IPv4 and IPv6. + * + * @param inside + * client IPs contained in `network`, including the `/64` keys the HTTP rate limiter groups + * IPv6 clients into. + * @param host + * a client IP contained in `network` that is also used as a single host override. + * @param otherHost + * another client IP of `network` that is not covered by the `host` override. + */ + private[util] final case class IpVersion( + name: String, + network: String, + inside: Seq[String], + outside: Seq[String], + host: String, + otherHost: String, + anyNetwork: String, + invalidCidrs: Seq[String], + normalizations: Seq[(String, String)], + ) + + private[util] val Ipv4: IpVersion = IpVersion( + name = "IPv4", + network = "10.0.0.0/8", + inside = Seq("10.1.2.3", "10.255.255.255", "10.0.0.0"), + outside = Seq("11.0.0.1", "9.255.255.255"), + host = "10.1.2.3", + otherHost = "10.1.2.4", + anyNetwork = "0.0.0.0/0", + invalidCidrs = Seq( + "not-an-ip/8", + "10.0.0.0/33", + "10.0.0.0/-1", + "10.0.0.0/eight", + "10.0.0.0/8/16", + "10.0.0.256/8", + "", + ), + normalizations = Seq( + "10.1.2.3/8" -> "10.0.0.0/8", + "10.1.2.3" -> "10.1.2.3/32", + "10.1.2.3/32" -> "10.1.2.3/32", + "10.1.2.3/0" -> "0.0.0.0/0", + "10.1.2.3/23" -> "10.1.2.0/23", + ), + ) + + private[util] val Ipv6: IpVersion = IpVersion( + name = "IPv6", + network = "2001:db8::/32", + // the HTTP rate limiter groups IPv6 clients into /64s, so both forms must match + inside = Seq("2001:db8:0:0:0:0:0:0/64", "2001:db8:1:2:3:4:5:6", "2001:db8::"), + outside = Seq("2001:db9:0:0:0:0:0:0/64", "2001:db9::1"), + host = "2001:db8:1:2:3:4:5:6", + otherHost = "2001:db8:1:2:3:4:5:7", + anyNetwork = "::/0", + invalidCidrs = Seq( + "not-an-ip/32", + "2001:db8::/129", + "2001:db8::/-1", + "2001:db8::/thirtytwo", + "2001:db8::/32/64", + "2001:db8:::1/32", + "", + ), + normalizations = Seq( + "2001:db8:1:2:3:4:5:6/32" -> "2001:db8:0:0:0:0:0:0/32", + "2001:db8:1:2:3:4:5:6/96" -> "2001:db8:1:2:3:4:0:0/96", + "2001:db8:1:2:3:4:5:6/0" -> "0:0:0:0:0:0:0:0/0", + "2001:db8:1:2:3:4:5:6" -> "2001:db8:1:2:3:4:5:6/128", + "2001:db8:1:2:3:4:5:6/64" -> "2001:db8:1:2:0:0:0:0/64", + ), + ) + + private[util] val IpVersions: Seq[IpVersion] = Seq(Ipv4, Ipv6) +} diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala index d673e1ff0ed..b13c24d7f9d 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/SpliceRateLimiterTest.scala @@ -97,35 +97,33 @@ class SpliceRateLimiterTest } } - } - - "the per attribute rate limiter" should { - - "gate the per attribute limit on the overall limiter being enabled" in { - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 42)) - .rateLimitFor(SpliceRateLimitConfig(ratePerSecond = 100)) - .ratePerSecond should be(42d) + "reject everything if the rate is zero" in { + withRateLimiter(SpliceRateLimitConfig(ratePerSecond = 0)) { + case (rateLimitMetrics, rateLimiter) => + Seq.fill(100)(rateLimiter.markRun()) should contain only false + + rateLimitMetrics.meter.valueFilteredOnLabels( + LabelFilter("limiter", "test"), + LabelFilter("result", "rejected"), + ) should be(100) + } + } - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 42)) - .rateLimitFor(SpliceRateLimitConfig(enabled = false, ratePerSecond = 100)) - .enabled should be(false) + "reject everything if the sustained rate is zero" in { + withRateLimiter( + SpliceRateLimitConfig(ratePerSecond = 100, sustainedRatePerSecond = Some(0)) + ) { case (_, rateLimiter) => + Seq.fill(100)(rateLimiter.markRun()) should contain only false + } + } - PerAttributeRateLimitConfig( - enabled = false, - limit = SpliceRateLimitConfig(ratePerSecond = 42), - ) - .rateLimitFor(SpliceRateLimitConfig(ratePerSecond = 100)) - .enabled should be(false) + } - PerAttributeRateLimitConfig.Disabled - .rateLimitFor(SpliceRateLimitConfig(ratePerSecond = 100)) - .enabled should be(false) - } + "the per attribute rate limiter" should { "limit each attribute value separately" in { withPerAttributeRateLimiter( - SpliceRateLimitConfig(ratePerSecond = 10), - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)) ) { case (_, perAttributeRateLimiter) => val ip1 = Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) ip1.count(identity) should be(2) @@ -140,8 +138,7 @@ class SpliceRateLimiterTest "not limit requests with an unknown attribute value" in { withPerAttributeRateLimiter( - SpliceRateLimitConfig(ratePerSecond = 10), - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)) ) { case (metrics, perAttributeRateLimiter) => // requests without an attribute value are not rate limited here; the overall/global // rate limiter is relied upon to bound them instead @@ -165,8 +162,7 @@ class SpliceRateLimiterTest "distinguish the metrics of the per attribute limiters" in { withPerAttributeRateLimiter( - SpliceRateLimitConfig(ratePerSecond = 10), - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 1)) ) { case (metrics, perAttributeRateLimiter) => val results = Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) @@ -190,19 +186,16 @@ class SpliceRateLimiterTest } "not limit anything if disabled" in { - withPerAttributeRateLimiter( - SpliceRateLimitConfig(ratePerSecond = 10), - PerAttributeRateLimitConfig.Disabled, - ) { case (_, perAttributeRateLimiter) => - Seq.fill(100)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) should contain only true - Seq.fill(100)(perAttributeRateLimiter.markRun(None)) should contain only true + withPerAttributeRateLimiter(PerAttributeRateLimitConfig.disabled) { + case (_, perAttributeRateLimiter) => + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) should contain only true + Seq.fill(100)(perAttributeRateLimiter.markRun(None)) should contain only true } } "respect the configured rate over time" in { withPerAttributeRateLimiter( - SpliceRateLimitConfig(ratePerSecond = 100), - PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 10)), + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 10)) ) { case (_, perAttributeRateLimiter) => // 10 per second per attribute value val results = runRateLimited(50, 100) { @@ -220,6 +213,189 @@ class SpliceRateLimiterTest } } + "reject all requests with an attribute value if the per attribute rate is zero" in { + withPerAttributeRateLimiter( + PerAttributeRateLimitConfig(limit = SpliceRateLimitConfig(ratePerSecond = 0)) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) should contain only false + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("2.2.2.2"))) should contain only false + // requests without an attribute value are still not limited here + Seq.fill(20)(perAttributeRateLimiter.markRun(None)) should contain only true + } + } + + } + + "the per attribute rate limiter with attribute overrides" should { + + "use the custom limit of a matching attribute value" in { + withPerAttributeRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = Map("1.1.1.1" -> SpliceRateLimitConfig(ratePerSecond = 3)), + ) + ) { case (_, perAttributeRateLimiter) => + // the override grants 3 permits (plus guava's deferred payment) to the matching value + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))).count(identity) should be(4) + // other attribute values use the default limit + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("2.2.2.2"))).count(identity) should be(2) + } + } + + "exempt matching attribute values if the override is disabled" in { + withPerAttributeRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = + Map("1.1.1.1" -> SpliceRateLimitConfig(enabled = false, ratePerSecond = 0)), + ) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) should contain only true + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("2.2.2.2"))).count(identity) should be(2) + } + } + + "not apply overrides if per attribute limiting is disabled" in { + withPerAttributeRateLimiter( + PerAttributeRateLimitConfig( + enabled = false, + attributeOverrides = Map("1.1.1.1" -> SpliceRateLimitConfig(ratePerSecond = 1)), + ) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))) should contain only true + } + } + + "use a custom matcher" in { + val prefixMatcher + : PerAttributeRateLimitConfig => String => Option[SpliceRateLimitConfig.Simple] = + config => + attributeValue => + config.attributeOverrides.collectFirst { + case (prefix, limit) if attributeValue.startsWith(prefix) => limit + } + + withPerAttributeRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = Map("premium-" -> SpliceRateLimitConfig(ratePerSecond = 3)), + ), + prefixMatcher, + ) { case (_, perAttributeRateLimiter) => + // every matching attribute value gets its own limiter with the overridden limit + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("premium-1"))).count(identity) should be( + 4 + ) + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("premium-2"))).count(identity) should be( + 4 + ) + + // non-matching attribute values use the default limit + Seq + .fill(20)(perAttributeRateLimiter.markRun(Some("standard-1"))) + .count(identity) should be(2) + } + } + + "not apply any override with the noOverrides matcher" in { + withPerAttributeRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = Map("1.1.1.1" -> SpliceRateLimitConfig(ratePerSecond = 100)), + ), + PerAttributeRateLimiter.noOverrides, + ) { case (_, perAttributeRateLimiter) => + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("1.1.1.1"))).count(identity) should be(2) + } + } + } + + "the per attribute rate limiter with IP CIDR overrides" should { + + "use the custom limit for IPs of a matching network" in { + withClientIpRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = Map("10.0.0.0/8" -> SpliceRateLimitConfig(ratePerSecond = 3)), + ) + ) { case (_, perAttributeRateLimiter) => + // the override grants 3 permits (plus guava's deferred payment) to every single matching IP + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("10.1.2.3"))).count(identity) should be(4) + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("10.4.5.6"))).count(identity) should be(4) + + // non-matching IPs use the default limit + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("11.1.2.3"))).count(identity) should be(2) + } + } + + "exempt matching IPs if the override is disabled" in { + withClientIpRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = + Map("10.0.0.0/8" -> SpliceRateLimitConfig(enabled = false, ratePerSecond = 0)), + ) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("10.1.2.3"))) should contain only true + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("11.1.2.3"))).count(identity) should be(2) + } + } + + "block matching IPs entirely if the override rate is zero" in { + withClientIpRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = Map("10.0.0.0/8" -> SpliceRateLimitConfig(ratePerSecond = 0)), + ) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("10.1.2.3"))) should contain only false + // non-matching IPs still use the default limit + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("11.1.2.3"))).count(identity) should be(2) + } + } + + "only limit matching IPs if the default limit is disabled" in { + withClientIpRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(enabled = false, ratePerSecond = 0), + attributeOverrides = Map("10.0.0.0/8" -> SpliceRateLimitConfig(ratePerSecond = 1)), + ) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("10.1.2.3"))).count(identity) should be(2) + Seq.fill(100)(perAttributeRateLimiter.markRun(Some("11.1.2.3"))) should contain only true + } + } + + "use the most specific matching network" in { + withClientIpRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 100), + attributeOverrides = Map( + "10.0.0.0/8" -> SpliceRateLimitConfig(ratePerSecond = 3), + "10.1.2.3" -> SpliceRateLimitConfig(ratePerSecond = 1), + ), + ) + ) { case (_, perAttributeRateLimiter) => + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("10.1.2.3"))).count(identity) should be(2) + Seq.fill(20)(perAttributeRateLimiter.markRun(Some("10.1.2.4"))).count(identity) should be(4) + } + } + + "limit IPs without a matching network with the default limit" in { + withClientIpRateLimiter( + PerAttributeRateLimitConfig( + limit = SpliceRateLimitConfig(ratePerSecond = 1), + attributeOverrides = Map("2001:db8::/32" -> SpliceRateLimitConfig(ratePerSecond = 3)), + ) + ) { case (_, perAttributeRateLimiter) => + Seq + .fill(20)(perAttributeRateLimiter.markRun(Some("2001:db8:0:0:0:0:0:0/64"))) + .count(identity) should be(4) + Seq + .fill(20)(perAttributeRateLimiter.markRun(Some("2001:db9:0:0:0:0:0:0/64"))) + .count(identity) should be(2) + } + } } "the rate limiter with a sustained limit" should { @@ -270,8 +446,10 @@ class SpliceRateLimiterTest } private def withPerAttributeRateLimiter[A]( - config: SpliceRateLimitConfig, - attributeConfig: PerAttributeRateLimitConfig, + config: PerAttributeRateLimitConfig, + attributeMatcher: PerAttributeRateLimitConfig => String => Option[ + SpliceRateLimitConfig.Simple + ] = PerAttributeRateLimiter.exactMatch, )(f: (SpliceRateLimitMetrics, PerAttributeRateLimiter) => A): A = { val metricsFactory = new InMemoryMetricsFactory() val rateLimitMetrics = SpliceRateLimitMetrics(metricsFactory, logger)(MetricsContext.Empty) @@ -279,9 +457,9 @@ class SpliceRateLimiterTest "test", "test_attribute", config, - attributeConfig, rateLimitMetrics, logger, + attributeMatcher, ) try { f(rateLimitMetrics, rateLimiter) @@ -289,6 +467,12 @@ class SpliceRateLimiterTest rateLimitMetrics.close() } } + + /** A limiter keyed by the client IP, i.e. one whose overrides are keyed by an IP network. */ + private def withClientIpRateLimiter[A]( + config: PerAttributeRateLimitConfig + )(f: (SpliceRateLimitMetrics, PerAttributeRateLimiter) => A): A = + withPerAttributeRateLimiter(config, IpCidrRateLimits.matchClientIp)(f) } object SpliceRateLimiterTest { diff --git a/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala b/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala index cd749f5548e..b8b419e70bb 100644 --- a/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala +++ b/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResourcesGenerator.scala @@ -18,12 +18,12 @@ object DarResourcesGenerator { // TODO(tech-debt): consider moving this to a dedicated config file if it bugs us here private val minimumInitializations: Map[String, String] = Map( - "splice-amulet" -> "0.1.15", - "splice-amulet-name-service" -> "0.1.16", - "splice-dso-governance" -> "0.1.21", - "splice-wallet" -> "0.1.15", - "splice-wallet-payments" -> "0.1.15", - "splitwell" -> "0.1.15", + "splice-amulet" -> "0.1.19", + "splice-amulet-name-service" -> "0.1.20", + "splice-dso-governance" -> "0.1.25", + "splice-wallet" -> "0.1.20", + "splice-wallet-payments" -> "0.1.19", + "splitwell" -> "0.1.20", "splice-validator-lifecycle" -> "0.1.5", "splice-util-batched-markers" -> "1.0.0", "splice-api-token-metadata-v1" -> "1.0.0", diff --git a/apps/package-lock.json b/apps/package-lock.json index 0493fa7e550..024e6d0e502 100644 --- a/apps/package-lock.json +++ b/apps/package-lock.json @@ -26,13 +26,13 @@ "wallet/external-openapi-ts-client" ], "dependencies": { - "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.23", - "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.22", - "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.28", - "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.8", - "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.23", - "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.22", - "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.23", + "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.24", + "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.23", + "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.29", + "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.9", + "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.24", + "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.23", + "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.24", "@daml/types": "^3.5.2", "@dmsnell/diff-match-patch": "^1.1.0", "dompurify": "^3.4.2", @@ -647,6 +647,32 @@ "common/frontend/daml.js/splice-amulet-0.1.22": { "name": "@daml.js/splice-amulet-0.1.22", "version": "3.5.2", + "extraneous": true, + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Set-Types-1.0.0": "file:../daml-stdlib-DA-Set-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-api-featured-app-v1-1.0.0": "file:../splice-api-featured-app-v1-1.0.0", + "@daml.js/splice-api-featured-app-v2-1.0.0": "file:../splice-api-featured-app-v2-1.0.0", + "@daml.js/splice-api-reward-assignment-v1-1.0.0": "file:../splice-api-reward-assignment-v1-1.0.0", + "@daml.js/splice-api-token-allocation-instruction-v1-1.0.0": "file:../splice-api-token-allocation-instruction-v1-1.0.0", + "@daml.js/splice-api-token-allocation-instruction-v2-1.0.0": "file:../splice-api-token-allocation-instruction-v2-1.0.0", + "@daml.js/splice-api-token-allocation-v1-1.0.0": "file:../splice-api-token-allocation-v1-1.0.0", + "@daml.js/splice-api-token-allocation-v2-1.0.0": "file:../splice-api-token-allocation-v2-1.0.0", + "@daml.js/splice-api-token-holding-v1-1.0.0": "file:../splice-api-token-holding-v1-1.0.0", + "@daml.js/splice-api-token-holding-v2-1.0.0": "file:../splice-api-token-holding-v2-1.0.0", + "@daml.js/splice-api-token-metadata-v1-1.0.0": "file:../splice-api-token-metadata-v1-1.0.0", + "@daml.js/splice-api-token-transfer-events-v2-1.0.0": "file:../splice-api-token-transfer-events-v2-1.0.0", + "@daml.js/splice-api-token-transfer-instruction-v1-1.0.0": "file:../splice-api-token-transfer-instruction-v1-1.0.0", + "@daml.js/splice-api-token-transfer-instruction-v2-1.0.0": "file:../splice-api-token-transfer-instruction-v2-1.0.0", + "@mojotech/json-type-validation": "^3.1.0" + } + }, + "common/frontend/daml.js/splice-amulet-0.1.23": { + "name": "@daml.js/splice-amulet-0.1.23", + "version": "3.5.2", "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -868,6 +894,7 @@ "common/frontend/daml.js/splice-amulet-name-service-0.1.23": { "name": "@daml.js/splice-amulet-name-service-0.1.23", "version": "3.5.2", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", @@ -878,6 +905,19 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-amulet-name-service-0.1.24": { + "name": "@daml.js/splice-amulet-name-service-0.1.24", + "version": "3.5.2", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.23": "file:../splice-amulet-0.1.23", + "@daml.js/splice-api-featured-app-v1-1.0.0": "file:../splice-api-featured-app-v1-1.0.0", + "@daml.js/splice-wallet-payments-0.1.23": "file:../splice-wallet-payments-0.1.23", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-amulet-name-service-0.1.9": { "name": "@daml.js/splice-amulet-name-service-0.1.9", "version": "0.0.0", @@ -1216,6 +1256,7 @@ "common/frontend/daml.js/splice-dso-governance-0.1.28": { "name": "@daml.js/splice-dso-governance-0.1.28", "version": "3.5.2", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -1228,6 +1269,21 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-dso-governance-0.1.29": { + "name": "@daml.js/splice-dso-governance-0.1.29", + "version": "3.5.2", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Set-Types-1.0.0": "file:../daml-stdlib-DA-Set-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.23": "file:../splice-amulet-0.1.23", + "@daml.js/splice-amulet-name-service-0.1.24": "file:../splice-amulet-name-service-0.1.24", + "@daml.js/splice-wallet-payments-0.1.23": "file:../splice-wallet-payments-0.1.23", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-validator-lifecycle-0.1.3": { "name": "@daml.js/splice-validator-lifecycle-0.1.3", "version": "0.0.0", @@ -1281,6 +1337,16 @@ "common/frontend/daml.js/splice-validator-lifecycle-0.1.8": { "name": "@daml.js/splice-validator-lifecycle-0.1.8", "version": "3.5.2", + "extraneous": true, + "license": "UNLICENSED", + "dependencies": { + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@mojotech/json-type-validation": "^3.1.0" + } + }, + "common/frontend/daml.js/splice-validator-lifecycle-0.1.9": { + "name": "@daml.js/splice-validator-lifecycle-0.1.9", + "version": "3.5.2", "license": "UNLICENSED", "dependencies": { "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", @@ -1522,6 +1588,7 @@ "common/frontend/daml.js/splice-wallet-0.1.23": { "name": "@daml.js/splice-wallet-0.1.23", "version": "3.5.2", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -1540,6 +1607,27 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-wallet-0.1.24": { + "name": "@daml.js/splice-wallet-0.1.24", + "version": "3.5.2", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.23": "file:../splice-amulet-0.1.23", + "@daml.js/splice-api-reward-assignment-v1-1.0.0": "file:../splice-api-reward-assignment-v1-1.0.0", + "@daml.js/splice-api-token-allocation-instruction-v1-1.0.0": "file:../splice-api-token-allocation-instruction-v1-1.0.0", + "@daml.js/splice-api-token-allocation-instruction-v2-1.0.0": "file:../splice-api-token-allocation-instruction-v2-1.0.0", + "@daml.js/splice-api-token-allocation-request-v2-1.0.0": "file:../splice-api-token-allocation-request-v2-1.0.0", + "@daml.js/splice-api-token-allocation-v1-1.0.0": "file:../splice-api-token-allocation-v1-1.0.0", + "@daml.js/splice-api-token-allocation-v2-1.0.0": "file:../splice-api-token-allocation-v2-1.0.0", + "@daml.js/splice-api-token-transfer-instruction-v1-1.0.0": "file:../splice-api-token-transfer-instruction-v1-1.0.0", + "@daml.js/splice-api-token-transfer-instruction-v2-1.0.0": "file:../splice-api-token-transfer-instruction-v2-1.0.0", + "@daml.js/splice-wallet-payments-0.1.23": "file:../splice-wallet-payments-0.1.23", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-wallet-0.1.9": { "name": "@daml.js/splice-wallet-0.1.9", "version": "0.0.0", @@ -1716,6 +1804,7 @@ "common/frontend/daml.js/splice-wallet-payments-0.1.22": { "name": "@daml.js/splice-wallet-payments-0.1.22", "version": "3.5.2", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -1725,6 +1814,18 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-wallet-payments-0.1.23": { + "name": "@daml.js/splice-wallet-payments-0.1.23", + "version": "3.5.2", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.23": "file:../splice-amulet-0.1.23", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-wallet-payments-0.1.9": { "name": "@daml.js/splice-wallet-payments-0.1.9", "version": "0.0.0", @@ -1923,6 +2024,7 @@ "common/frontend/daml.js/splitwell-0.1.23": { "name": "@daml.js/splitwell-0.1.23", "version": "3.5.2", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -1933,6 +2035,19 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splitwell-0.1.24": { + "name": "@daml.js/splitwell-0.1.24", + "version": "3.5.2", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.23": "file:../splice-amulet-0.1.23", + "@daml.js/splice-wallet-payments-0.1.23": "file:../splice-wallet-payments-0.1.23", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splitwell-0.1.9": { "name": "@daml.js/splitwell-0.1.9", "version": "0.0.0", @@ -2426,7 +2541,7 @@ } }, "node_modules/@daml.js/ans": { - "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.23", + "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.24", "link": true }, "node_modules/@daml.js/daml-prim-DA-Types-1.0.0": { @@ -2446,15 +2561,15 @@ "link": true }, "node_modules/@daml.js/splice-amulet": { - "resolved": "common/frontend/daml.js/splice-amulet-0.1.22", + "resolved": "common/frontend/daml.js/splice-amulet-0.1.23", "link": true }, - "node_modules/@daml.js/splice-amulet-0.1.22": { - "resolved": "common/frontend/daml.js/splice-amulet-0.1.22", + "node_modules/@daml.js/splice-amulet-0.1.23": { + "resolved": "common/frontend/daml.js/splice-amulet-0.1.23", "link": true }, - "node_modules/@daml.js/splice-amulet-name-service-0.1.23": { - "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.23", + "node_modules/@daml.js/splice-amulet-name-service-0.1.24": { + "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.24", "link": true }, "node_modules/@daml.js/splice-api-featured-app-v1-1.0.0": { @@ -2534,27 +2649,27 @@ "link": true }, "node_modules/@daml.js/splice-dso-governance": { - "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.28", + "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.29", "link": true }, "node_modules/@daml.js/splice-validator-lifecycle": { - "resolved": "common/frontend/daml.js/splice-validator-lifecycle-0.1.8", + "resolved": "common/frontend/daml.js/splice-validator-lifecycle-0.1.9", "link": true }, "node_modules/@daml.js/splice-wallet": { - "resolved": "common/frontend/daml.js/splice-wallet-0.1.23", + "resolved": "common/frontend/daml.js/splice-wallet-0.1.24", "link": true }, "node_modules/@daml.js/splice-wallet-payments": { - "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.22", + "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.23", "link": true }, - "node_modules/@daml.js/splice-wallet-payments-0.1.22": { - "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.22", + "node_modules/@daml.js/splice-wallet-payments-0.1.23": { + "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.23", "link": true }, "node_modules/@daml.js/splitwell": { - "resolved": "common/frontend/daml.js/splitwell-0.1.23", + "resolved": "common/frontend/daml.js/splitwell-0.1.24", "link": true }, "node_modules/@daml/types": { diff --git a/apps/package.json b/apps/package.json index e5bb49b600e..8d9752052ef 100644 --- a/apps/package.json +++ b/apps/package.json @@ -21,13 +21,13 @@ "wallet/external-openapi-ts-client" ], "dependencies": { - "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.23", - "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.22", - "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.28", - "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.8", - "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.23", - "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.22", - "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.23", + "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.24", + "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.23", + "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.29", + "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.9", + "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.24", + "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.23", + "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.24", "@daml/types": "^3.5.2", "@dmsnell/diff-match-patch": "^1.1.0", "dompurify": "^3.4.2", diff --git a/apps/scan/frontend/src/__tests__/mocks/data.ts b/apps/scan/frontend/src/__tests__/mocks/data.ts index f4ded63db46..f63780c1e10 100644 --- a/apps/scan/frontend/src/__tests__/mocks/data.ts +++ b/apps/scan/frontend/src/__tests__/mocks/data.ts @@ -180,6 +180,9 @@ export function amuletRules(zeroTransferFees: boolean): any { externalPartyConfigStateTickDuration: null, rewardConfig: null, transferPreapprovalBaseDuration: null, + developmentFundManagerBlacklist: null, + minDevelopmentFundMintingDelay: null, + amuletSwitchOverTimes: null, }, futureValues: [], }, diff --git a/apps/scan/src/main/openapi/scan.yaml b/apps/scan/src/main/openapi/scan.yaml index efbc94a9f4f..7249cc8b881 100644 --- a/apps/scan/src/main/openapi/scan.yaml +++ b/apps/scan/src/main/openapi/scan.yaml @@ -4060,6 +4060,9 @@ components: update_id: description: | The ID of the transaction update associated with this verdict. + + Verdicts are deduplicated by update_id. Only the first verdict ingested for a given update_id is stored and returned, while a subsequent verdict with the same update_id is rejected. + This can happen for example if a sequencer client retries a successful submission. In that case, the retry is rejected as a duplicate, and the events endpoint will only show the successful verdict but not the rejected duplicates. type: string migration_id: description: | diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala index b07eb2f3b6a..4955d4d8d5e 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/ScanApp.scala @@ -283,6 +283,7 @@ class ScanApp( config.activityIngestionUserVersion.fold(0)(_.toInt), ), config.isFirstSv, + initialRound.toLong, loggerFactory, ) appRewardsStore = new DbScanAppRewardsStore( 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 30802acef6c..372b89abe5f 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 @@ -382,20 +382,25 @@ class BftScanConnection( .map(result => connection -> result) ) } yield { - val withData = results.collect { - case (connection, BftScanConnection.SuccessfulResponse(Some(info))) => connection -> info - }.toMap - val withoutData = results.collect { - case (connection, BftScanConnection.SuccessfulResponse(None)) => connection - }.toSet - val unknownStatus = results.collect { - case (connection, BftScanConnection.HttpFailureResponse(_, _)) => connection - case (connection, BftScanConnection.ExceptionFailureResponse(_)) => connection - }.toSet + val (withData, other) = + results.partitionMap { case (connection, response) => + response match { + case BftScanConnection.SuccessfulResponse(Some(info)) => + Left(connection -> info) + case BftScanConnection.SuccessfulResponse(None) => + Right(Left(connection)) + case _: BftScanConnection.HttpFailureResponse[?] | + _: BftScanConnection.NonJsonHttpFailureResponse[?] | + _: BftScanConnection.TextFailureResponse[?] | + _: BftScanConnection.ExceptionFailureResponse[?] => + Right(Right(connection)) + } + } + val (withoutData, unknownStatus) = other partitionMap identity MigrationInfoResponses( - withData, - withoutData, - unknownStatus, + withData.toMap, + withoutData.toSet, + unknownStatus.toSet, ) } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpTokenStandardTransferInstructionHandler.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpTokenStandardTransferInstructionHandler.scala index 7b0f3b3d218..ee216a5694a 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpTokenStandardTransferInstructionHandler.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpTokenStandardTransferInstructionHandler.scala @@ -75,6 +75,7 @@ class HttpTokenStandardTransferInstructionHandler( choiceContextBuilder <- getAmuletRulesTransferContextV1( body.excludeDebugFields.getOrElse(false) ) + dsoRules <- store.getDsoRules() result <- buildTransferFactory( PartyId.tryFromProtoPrimitive(transferInstr.transfer.sender), PartyId.tryFromProtoPrimitive(transferInstr.transfer.receiver), @@ -92,9 +93,9 @@ class HttpTokenStandardTransferInstructionHandler( kind, choiceContext = choiceContextBuilder .addOptionalContracts( - "featured-app-right" -> optFeaturedAppRight, - "transfer-preapproval" -> optTransferPreapproval, + "transfer-preapproval" -> optTransferPreapproval ) + .addFeaturedAppRight(clock, dsoRules.payload, optFeaturedAppRight) .disclose(externalPartyAmuletRules.contract) .build(), ) @@ -183,6 +184,7 @@ class HttpTokenStandardTransferInstructionHandler( choiceContextBuilder <- getAmuletRulesTransferContextV2( body.excludeDebugFields.getOrElse(false) ) + dsoRules <- store.getDsoRules() result <- buildTransferFactory( PartyId.tryFromProtoPrimitive( TokenStandardAccount.tryGetRegularAccountOwner(transferInstr.transfer.sender) @@ -204,9 +206,9 @@ class HttpTokenStandardTransferInstructionHandler( kind, choiceContext = choiceContextBuilder .addOptionalContracts( - "featured-app-right" -> optFeaturedAppRight, - "transfer-preapproval" -> optTransferPreapproval, + "transfer-preapproval" -> optTransferPreapproval ) + .addFeaturedAppRight(clock, dsoRules.payload, optFeaturedAppRight) .disclose(externalPartyAmuletRules.contract) .build(), ) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala index 5a8da6401bc..d7ac8267b6d 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionService.scala @@ -11,7 +11,7 @@ import com.digitalasset.base.error.utils.ErrorDetails import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.lifecycle.{AsyncOrSyncCloseable, SyncCloseable} -import com.digitalasset.canton.logging.NamedLoggerFactory +import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} import com.digitalasset.canton.mediator.admin.v30 import com.digitalasset.canton.sequencing.traffic.TrafficControlErrors import com.digitalasset.canton.time.Clock @@ -64,6 +64,40 @@ object ScanVerdictIngestionService { .filter(_ >= start) .filterNot(summaryTimes.contains) } + + /** Groups a batch of verdicts by update id and returns only the update ids that + * appear more than once within the batch. + */ + def findDuplicateUpdateIds(batch: Seq[v30.Verdict]): Map[String, Seq[(v30.Verdict, Int)]] = + batch.zipWithIndex.groupBy(_._1.updateId).filter(_._2.size > 1) + + /** True if any verdict in the duplicate groups has an accept after another verdict. */ + def duplicatesContainSubsequentAccept(duplicates: Map[String, Seq[(v30.Verdict, Int)]]): Boolean = + duplicates.exists { case (_, group) => + group.drop(1).exists(_._1.verdict == v30.VerdictResult.VERDICT_RESULT_ACCEPTED) + } + + /** Finds and logs when duplicate verdicts exist in a batch. + * Logs at WARN level when any duplicate is an accept, otherwise at INFO level. + */ + def logDuplicateUpdateIds(batch: Seq[v30.Verdict], logger: TracedLogger)(implicit + tc: TraceContext + ): Unit = { + val duplicates = findDuplicateUpdateIds(batch) + if (duplicates.nonEmpty) { + val message = s"Received multiple verdicts with the same update id in the same batch. " + + s"Batch: ${batch.size} verdicts with record times ${batch.map(_.getRecordTime).map(CantonTimestamp.tryFromProtoTimestamp).mkString("[", ",", "]")}. " + + s"Duplicate verdicts: ${duplicates.values.flatten + .map { case (verdict, index) => + s"${index} => ${verdict}" + } + .mkString("[\n", ",\n", "\n]")}" + + if (duplicatesContainSubsequentAccept(duplicates)) + logger.warn(s"$message Duplicate verdicts contains a subsequent accept.") + else logger.info(message) + } + } } class ScanVerdictIngestionService( @@ -387,22 +421,7 @@ class ScanVerdictIngestionService( .batch(math.max(1, config.mediatorVerdictIngestion.batchSize.toLong), Vector(_))(_ :+ _) // TODO(DACH-NY/cn-test-failures#8281): Remove once we have figured out why we're getting duplicate data. .map(batch => { - val duplicates = batch.zipWithIndex - .groupBy(_._1.updateId) - .filter(_._2.size > 1) - - if (duplicates.nonEmpty) { - logger.info( - s"Received multiple verdicts with the same update id in the same batch. " + - s"Batch: ${batch.size} verdicts with record times ${batch.map(_.getRecordTime).map(CantonTimestamp.tryFromProtoTimestamp).mkString("[", ",", "]")}. " + - s"Duplicate verdicts: ${duplicates.values.flatten - .map { case (verdict, index) => - s"${index} => ${verdict}" - } - .mkString("[\n", ",\n", "\n]")}" - ) - } - + ScanVerdictIngestionService.logDuplicateUpdateIds(batch, logger) batch }) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala index 81721f93938..d155834b25f 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/AcsSnapshotStore.scala @@ -5,6 +5,7 @@ package org.lfdecentralizedtrust.splice.scan.store import cats.data.NonEmptyVector import com.daml.ledger.javaapi.data.CreatedEvent +import com.daml.nonempty.NonEmpty import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.{Amulet, LockedAmulet} import org.lfdecentralizedtrust.splice.scan.store.AcsSnapshotStore.{ AcsSnapshot, @@ -290,18 +291,18 @@ class AcsSnapshotStore( case None => Future.successful(snapshot.firstRowId) } end = snapshot.lastRowId - partyIdsFilter = partyIds match { - case Nil => + partyIdsFilter = NonEmpty.from(partyIds) match { + case None => // This expression is always true (scan only processes data where the DSO is stakeholder). // It is included to make sure the query plan uses the right index (acs_snapshot_data_all_filters) sql"and stakeholder = ${dsoParty}" - case partyIds => - (sql" and " ++ inClause("stakeholder", partyIds)).toActionBuilder + case Some(partyIds) => + (sql" and " ++ DbStorage.toInClause("stakeholder", partyIds)).toActionBuilder } - templatesFilter = templates match { - case Nil => sql"" - case _ => - (sql" and " ++ inClause( + templatesFilter = NonEmpty.from(templates) match { + case None => sql"" + case Some(templates) => + (sql" and " ++ DbStorage.toInClause( "template_id", templates.map(t => lengthLimited( diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala index 60a3664e000..bdb30350c4b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbAppActivityRecordStore.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.scan.store.db +import com.daml.nonempty.NonEmpty import org.lfdecentralizedtrust.splice.scan.store.AppActivityStore import org.lfdecentralizedtrust.splice.scan.store.AppActivityStore.RoundIngestionStatus import org.lfdecentralizedtrust.splice.store.UpdateHistory @@ -94,6 +95,7 @@ class DbAppActivityRecordStore( updateHistory: UpdateHistory, val ingestionVersions: DbAppActivityRecordStore.IngestionVersions, isFirstSv: Boolean, + initialRound: Long, override protected val loggerFactory: NamedLoggerFactory, )(implicit ec: ExecutionContext @@ -286,22 +288,24 @@ class DbAppActivityRecordStore( def getRecordsByVerdictRowIds( verdictRowIds: Seq[Long] )(implicit tc: TraceContext): Future[Map[Long, AppActivityRecordT]] = { - if (verdictRowIds.isEmpty) Future.successful(Map.empty) - else { - startedIngestingAt.flatMap { - case None => Future.successful(Map.empty) - case Some(_) => - storage - .query( - (sql""" - select verdict_row_id, round_number, app_provider_parties, app_activity_weights - from #${Tables.appActivityRecords} - where history_id = $historyId and """ ++ inClause("verdict_row_id", verdictRowIds)) - .as[AppActivityRecordT], - "appActivity.getRecordsByVerdictRowIds", - ) - .map(rows => rows.map(r => r.verdictRowId -> r).toMap) - } + NonEmpty.from(verdictRowIds) match { + case None => Future.successful(Map.empty) + case Some(verdictRowIds) => + startedIngestingAt.flatMap { + case None => Future.successful(Map.empty) + case Some(_) => + storage + .query( + (sql""" + select verdict_row_id, round_number, app_provider_parties, app_activity_weights + from #${Tables.appActivityRecords} + where history_id = $historyId and """ ++ DbStorage + .toInClause("verdict_row_id", verdictRowIds)) + .as[AppActivityRecordT], + "appActivity.getRecordsByVerdictRowIds", + ) + .map(rows => rows.map(r => r.verdictRowId -> r).toMap) + } } } @@ -347,16 +351,16 @@ class DbAppActivityRecordStore( } // earliestRound: the oldest round open at the earliest record_time of this batch. - // or (-1) on firstSV, as it is expected to have complete data for the first round. - val earliestRound = if (isFirstSv) Some(-1L) else firstActiveRoundO + // or (initialRound - 1) on firstSV, so earliestRoundWithCompleteAppActivity() + // returns initialRound (correct for non-zero-round bootstrap). + val earliestRound = if (isFirstSv) Some(initialRound - 1) else firstActiveRoundO // lastArchived: the highest round archived as of this verdict batch. // - From the caller when available - // - Bootstrapped to 0 on a fresh firstSV because - // lookupLatestArchivedOpenMiningRound may not yet reflect - // round 0's archival due to ingestion delay. + // - Bootstrapped to initialRound on a fresh firstSV so the + // complete-activity window covers the first TBAR round val lastArchived = lastArchivedRoundO - .orElse(if (isFirstSv) Some(0L) else None) + .orElse(if (isFirstSv) Some(initialRound) else None) for { _ <- insertRecords diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala index 30f4db856e1..66b682a2ca3 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanAppRewardsStore.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.scan.store.db +import com.daml.nonempty.NonEmpty import org.lfdecentralizedtrust.splice.scan.rewards.{RewardComputationInputs, RewardIssuanceParams} import org.lfdecentralizedtrust.splice.scan.store.ScanAppRewardsStore import org.lfdecentralizedtrust.splice.store.UpdateHistory @@ -615,15 +616,16 @@ class DbScanAppRewardsStore( override def roundsWithComputedRewards(rounds: Seq[Long])(implicit tc: TraceContext ): Future[Set[Long]] = { - if (rounds.isEmpty) Future.successful(Set.empty) - else { - runQuery( - (sql"""select round_number from #${Tables.appRewardRootHashes} - where history_id = $historyId - and """ ++ inClause("round_number", rounds)).toActionBuilder - .as[Long], - "appRewards.roundsWithComputedRewards", - ).map(_.toSet) + NonEmpty.from(rounds) match { + case None => Future.successful(Set.empty) + case Some(rounds) => + runQuery( + (sql"""select round_number from #${Tables.appRewardRootHashes} + where history_id = $historyId + and """ ++ DbStorage.toInClause("round_number", rounds)).toActionBuilder + .as[Long], + "appRewards.roundsWithComputedRewards", + ).map(_.toSet) } } 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 871c2633b63..07bba5a6b92 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 @@ -4,6 +4,7 @@ package org.lfdecentralizedtrust.splice.scan.store.db import com.daml.ledger.javaapi.data.codegen.ContractId +import com.daml.nonempty.NonEmpty import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.{ AsyncOrSyncCloseable, @@ -496,24 +497,28 @@ class DbScanStore( override def getValidatorLicenseByValidator(validators: Vector[PartyId])(implicit tc: TraceContext ): Future[Seq[Contract[ValidatorLicense.ContractId, ValidatorLicense]]] = waitUntilAcsIngested { - val validatorPartyIds = inClause("validator", validators) - for { - rows <- storage - .query( - selectFromAcsTable( - ScanTables.acsTableName, - acsStoreId, - domainMigrationId, - ValidatorLicense.COMPANION, - where = validatorPartyIds, - ), - "getValidatorLicenseByValidator", - ) - } yield { - rows - .map( - contractFromRow(ValidatorLicense.COMPANION)(_) - ) + NonEmpty.from(validators) match { + case None => Future.successful(Seq.empty) + case Some(validators) => + val validatorPartyIds = DbStorage.toInClause("validator", validators) + for { + rows <- storage + .query( + selectFromAcsTable( + ScanTables.acsTableName, + acsStoreId, + domainMigrationId, + ValidatorLicense.COMPANION, + where = validatorPartyIds, + ), + "getValidatorLicenseByValidator", + ) + } yield { + rows + .map( + contractFromRow(ValidatorLicense.COMPANION)(_) + ) + } } } @@ -665,22 +670,26 @@ class DbScanStore( trackingCids: Seq[VoteRequest.ContractId], limit: Limit, )(implicit tc: TraceContext): Future[Seq[Contract[VoteRequest.ContractId, VoteRequest]]] = { - for { - result <- storage - .query( - listVoteRequestsByTrackingCidQuery( - acsTableName = ScanTables.acsTableName, - acsStoreId = acsStoreId, - domainMigrationId = domainMigrationId, - trackingCidColumnName = "vote_request_tracking_cid", - trackingCids = trackingCids, - limit = limit, - ), - "listVoteRequestsByTrackingCid", - ) - records = applyLimit("listVoteRequestsByTrackingCid", limit, result) - } yield records - .map(contractFromRow(VoteRequest.COMPANION)(_)) + NonEmpty.from(trackingCids) match { + case None => Future.successful(Seq.empty) + case Some(trackingCids) => + for { + result <- storage + .query( + listVoteRequestsByTrackingCidQuery( + acsTableName = ScanTables.acsTableName, + acsStoreId = acsStoreId, + domainMigrationId = domainMigrationId, + trackingCidColumnName = "vote_request_tracking_cid", + trackingCids = trackingCids, + limit = limit, + ), + "listVoteRequestsByTrackingCid", + ) + records = applyLimit("listVoteRequestsByTrackingCid", limit, result) + } yield records + .map(contractFromRow(VoteRequest.COMPANION)(_)) + } } override def lookupVoteRequest(voteRequestCid: VoteRequest.ContractId)(implicit diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala index bd565c986e4..365df1eee4a 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanVerdictStore.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.scan.store.db +import com.daml.nonempty.NonEmpty import org.lfdecentralizedtrust.splice.util.FutureUnlessShutdownUtil.futureUnlessShutdownToFuture import com.digitalasset.canton.sequencer.admin.{v30 as seqv30} import com.digitalasset.canton.data.CantonTimestamp @@ -410,41 +411,58 @@ class DbScanVerdictStore( def insertVerdictAndTransactionViewsDBIO( items: Seq[(VerdictT, Long => Seq[TransactionViewT])] )(implicit tc: TraceContext): DBIO[Map[CantonTimestamp, Long]] = { - if (items.isEmpty) DBIO.successful(Map.empty) - else { - val checkExist = (sql""" - select update_id - from #${Tables.verdicts} - where history_id = $historyId - and """ ++ inClause("update_id", items.map(t => lengthLimited(t._1.updateId)))) - .as[String] - - for { - alreadyExisting <- checkExist.map(_.toSet) - nonExisting = items.filter(item => !alreadyExisting.contains(item._1.updateId)) - _ = logger.info( - s"Already ingested verdicts: $alreadyExisting. Non-existing: ${nonExisting.map(_._1.updateId)}." - ) - rowIdMap <- - if (nonExisting.nonEmpty) { - DBIO - .sequence(nonExisting.map { case (verdict, mkViews) => - for { - idOpt <- sqlInsertVerdictReturningId(verdict) - rowId <- idOpt match { - case Some(id) => DBIO.successful(id) - case None => - DBIO.failed(new RuntimeException("insertVerdict did not return row_id")) - } - views = mkViews(rowId) - _ <- DBIO.sequence(views.map(sqlInsertView)).map(_ => ()) - } yield verdict.recordTime -> rowId - }) - .map(_.toMap) - } else { - DBIO.successful(Map.empty[CantonTimestamp, Long]) - } - } yield rowIdMap + NonEmpty.from(items) match { + case None => DBIO.successful(Map.empty) + case Some(items) => + val checkExist = (sql""" + select update_id + from #${Tables.verdicts} + where history_id = $historyId + and """ ++ DbStorage.toInClause( + "update_id", + items.map(t => lengthLimited(t._1.updateId)), + )) + .as[String] + + for { + alreadyExisting <- checkExist.map(_.toSet) + (dropped, nonExisting) = + items.partition(item => alreadyExisting.contains(item._1.updateId)) + droppedAccepts = + dropped.filter(_._1.verdictResult == DbScanVerdictStore.VerdictResultDbValue.Accepted) + nonExistingMessage = s"Non-existing: ${nonExisting.map(_._1.updateId)}." + _ = + if (droppedAccepts.nonEmpty) + logger.warn( + s"Dropping duplicate accepted verdicts: ${droppedAccepts.map(_._1.updateId)}. " + + s"All dropped verdicts: ${dropped.map(_._1.updateId)}. $nonExistingMessage" + ) + else if (dropped.nonEmpty) + logger.info( + s"Dropping duplicate verdicts: ${dropped.map(_._1.updateId)}. $nonExistingMessage" + ) + else + logger.info(s"Already ingested verdicts: $alreadyExisting. $nonExistingMessage") + rowIdMap <- + if (nonExisting.nonEmpty) { + DBIO + .sequence(nonExisting.map { case (verdict, mkViews) => + for { + idOpt <- sqlInsertVerdictReturningId(verdict) + rowId <- idOpt match { + case Some(id) => DBIO.successful(id) + case None => + DBIO.failed(new RuntimeException("insertVerdict did not return row_id")) + } + views = mkViews(rowId) + _ <- DBIO.sequence(views.map(sqlInsertView)).map(_ => ()) + } yield verdict.recordTime -> rowId + }) + .map(_.toMap) + } else { + DBIO.successful(Map.empty[CantonTimestamp, Long]) + } + } yield rowIdMap } } diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/ChoiceContextBuilder.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/ChoiceContextBuilder.scala index ad8a9b5ebe3..34f3a39ba01 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/ChoiceContextBuilder.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/util/ChoiceContextBuilder.scala @@ -8,6 +8,7 @@ import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet +import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.DsoRules import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.metadatav1 import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.metadatav1.AnyContract import org.lfdecentralizedtrust.splice.codegen.java.splice.round @@ -17,6 +18,7 @@ import org.lfdecentralizedtrust.splice.util.{ AmuletConfigSchedule, Contract, ContractWithState, + SwitchOverTimes, TokenStandardMetadata, } @@ -96,6 +98,22 @@ abstract class ChoiceContextBuilder[DisclosedContract, ChoiceContext, Self]( this } + def addFeaturedAppRight( + clock: Clock, + dsoRules: DsoRules, + optFeaturedAppRight: Option[ + ContractWithState[amulet.FeaturedAppRight.ContractId, amulet.FeaturedAppRight] + ], + ): Self = { + if (SwitchOverTimes.omitFeaturedAppRightInChoiceContext(clock, dsoRules)) { + this + } else { + addOptionalContract( + "featured-app-right" -> optFeaturedAppRight.map(_.contract) + ) + } + } + def build(): ChoiceContext } @@ -184,6 +202,7 @@ object ChoiceContextBuilder { provider ) ) + dsoRules <- store.getDsoRules() } yield { if (optLockedAmulet.isEmpty) { // the locked amulet did expire and was unlocked @@ -205,7 +224,7 @@ object ChoiceContextBuilder { choiceContextBuilder // the choice implementation should only attempt to expire the lock if it exists .addBool(TokenStandardMetadata.expireLockKey, optLockedAmulet.isDefined) - .addOptionalContract("featured-app-right", featuredAppRightO.map(_.contract)) + .addFeaturedAppRight(clock, dsoRules.payload, featuredAppRightO) .build() } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionServiceTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionServiceTest.scala index 30655dd023f..24b0da6f37f 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionServiceTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/automation/ScanVerdictIngestionServiceTest.scala @@ -1,13 +1,24 @@ package org.lfdecentralizedtrust.splice.scan.automation +import com.digitalasset.canton.BaseTest import com.digitalasset.canton.data.CantonTimestamp -import org.scalatest.matchers.should.Matchers +import com.digitalasset.canton.logging.SuppressionRule +import com.digitalasset.canton.mediator.admin.v30 import org.scalatest.wordspec.AnyWordSpec +import org.slf4j.event.Level.INFO -class ScanVerdictIngestionServiceTest extends AnyWordSpec with Matchers { +class ScanVerdictIngestionServiceTest extends AnyWordSpec with BaseTest { private def ts(micros: Long) = CantonTimestamp.ofEpochMicro(micros) + private def mkVerdict(updateId: String, accepted: Boolean): v30.Verdict = + v30.Verdict.defaultInstance.copy( + updateId = updateId, + verdict = + if (accepted) v30.VerdictResult.VERDICT_RESULT_ACCEPTED + else v30.VerdictResult.VERDICT_RESULT_REJECTED, + ) + "findMissingTrafficSummaries" should { "return empty when ingestion hasn't started" in { @@ -50,4 +61,96 @@ class ScanVerdictIngestionServiceTest extends AnyWordSpec with Matchers { ) shouldBe empty } } + + "findDuplicateUpdateIds" should { + + "return empty when all update ids are distinct" in { + ScanVerdictIngestionService.findDuplicateUpdateIds( + Seq(mkVerdict("a", true), mkVerdict("b", false)) + ) shouldBe empty + } + + "return only the update ids that appear more than once" in { + val result = ScanVerdictIngestionService.findDuplicateUpdateIds( + Seq(mkVerdict("a", true), mkVerdict("b", false), mkVerdict("a", false)) + ) + result.keySet shouldBe Set("a") + result("a").map(_._2) shouldBe Seq(0, 2) + } + } + + "duplicatesContainSubsequentAccept" should { + + "return false when no duplicate is an accept" in { + val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( + Seq(mkVerdict("a", false), mkVerdict("a", false)) + ) + ScanVerdictIngestionService.duplicatesContainSubsequentAccept(duplicates) shouldBe false + } + + "return true when a duplicate group contains an accept after another verdict" in { + val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( + Seq(mkVerdict("a", false), mkVerdict("a", true)) + ) + ScanVerdictIngestionService.duplicatesContainSubsequentAccept(duplicates) shouldBe true + } + + "return false when a duplicate group contains an accept before another verdict" in { + val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( + Seq(mkVerdict("a", true), mkVerdict("a", false)) + ) + ScanVerdictIngestionService.duplicatesContainSubsequentAccept(duplicates) shouldBe false + } + + "ignore an accept that is not part of a duplicate group" in { + // "a" is accepted but unique; only "b" is duplicated (both rejected). + val duplicates = ScanVerdictIngestionService.findDuplicateUpdateIds( + Seq(mkVerdict("a", true), mkVerdict("b", false), mkVerdict("b", false)) + ) + ScanVerdictIngestionService.duplicatesContainSubsequentAccept(duplicates) shouldBe false + } + } + + "logDuplicateUpdateIds" should { + + "not log when there are no duplicates" in { + loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(INFO))( + ScanVerdictIngestionService.logDuplicateUpdateIds( + Seq(mkVerdict("a", true), mkVerdict("b", true)), + logger, + ), + _ shouldBe empty, + ) + } + + "log at info level when no duplicate is an accept" in { + loggerFactory.assertLogs(SuppressionRule.LevelAndAbove(INFO))( + ScanVerdictIngestionService.logDuplicateUpdateIds( + Seq(mkVerdict("a", false), mkVerdict("a", false)), + logger, + ), + _.infoMessage should include("Duplicate verdicts:"), + ) + } + + "log at warning level when a duplicate is a subsequent accept" in { + loggerFactory.assertLogs( + ScanVerdictIngestionService.logDuplicateUpdateIds( + Seq(mkVerdict("a", false), mkVerdict("a", true)), + logger, + ), + _.warningMessage should endWith("Duplicate verdicts contains a subsequent accept."), + ) + } + + "log at info level when a duplicate is not a subsequent accept" in { + loggerFactory.assertLogs(SuppressionRule.LevelAndAbove(INFO))( + ScanVerdictIngestionService.logDuplicateUpdateIds( + Seq(mkVerdict("a", true), mkVerdict("a", false)), + logger, + ), + _.infoMessage should include("Duplicate verdicts:"), + ) + } + } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala index 30a98678784..c716f41a575 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbAppActivityRecordStoreTest.scala @@ -412,6 +412,101 @@ class DbAppActivityRecordStoreTest countAfter shouldBe 0L } } + + "drop a rejected verdict that's a duplicate of a prior accept" in { + val updateId = "update-dupe-reject-after-accept" + val ts1 = CantonTimestamp.now() + val ts2 = ts1.plusSeconds(1L) + for { + (appStore, verdictStore) <- newStores() + accepted = mkVerdict( + verdictStore, + updateId, + ts1, + DbScanVerdictStore.VerdictResultDbValue.Accepted, + ) + rejected = mkVerdict( + verdictStore, + updateId, + ts2, + DbScanVerdictStore.VerdictResultDbValue.Rejected, + ) + // First batch with the accepted verdict and its activity record + _ <- verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of(accepted -> noViews), + Seq(ts1 -> mkRecord(0L, 10L, Seq("app1::provider"), Seq(100L))), + hasTrafficSummaries = true, + firstActiveRoundO = Some(10L), + lastArchivedRoundO = Some(9L), + ) + countAfterBatch1 <- countRecords() + // A later batch with a rejection for the same update_id + _ <- verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of(rejected -> noViews), + Seq(ts2 -> mkRecord(0L, 11L, Seq("app2::provider"), Seq(200L))), + hasTrafficSummaries = true, + firstActiveRoundO = Some(11L), + lastArchivedRoundO = Some(10L), + ) + v <- verdictStore.getVerdictByUpdateId(updateId) + countAfterBatch2 <- countRecords() + } yield { + v shouldBe defined + v.value.verdictResult shouldBe DbScanVerdictStore.VerdictResultDbValue.Accepted + v.value.recordTime shouldBe ts1 + // The rejection's activity record was dropped along with the verdict + countAfterBatch2 shouldBe countAfterBatch1 + } + } + + "drop and warn of an accepted verdict that's a duplicate of a prior rejection" in { + val updateId = "update-dupe-accept-after-reject" + val ts1 = CantonTimestamp.now() + val ts2 = ts1.plusSeconds(1L) + for { + (appStore, verdictStore) <- newStores() + rejected = mkVerdict( + verdictStore, + updateId, + ts1, + DbScanVerdictStore.VerdictResultDbValue.Rejected, + ) + accepted = mkVerdict( + verdictStore, + updateId, + ts2, + DbScanVerdictStore.VerdictResultDbValue.Accepted, + ) + // First batch with the rejection and its activity record + _ <- verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of(rejected -> noViews), + Seq(ts1 -> mkRecord(0L, 10L, Seq("app1::provider"), Seq(100L))), + hasTrafficSummaries = true, + firstActiveRoundO = Some(10L), + lastArchivedRoundO = Some(9L), + ) + countAfterBatch1 <- countRecords() + // A later batch with an accept for the same update_id + _ <- loggerFactory.assertLogs( + verdictStore.insertVerdictsWithAppActivityRecords( + NonEmptyList.of(accepted -> noViews), + Seq(ts2 -> mkRecord(0L, 11L, Seq("app2::provider"), Seq(200L))), + hasTrafficSummaries = true, + firstActiveRoundO = Some(11L), + lastArchivedRoundO = Some(10L), + ), + _.warningMessage should startWith("Dropping duplicate accepted verdicts"), + ) + v <- verdictStore.getVerdictByUpdateId(updateId) + countAfterBatch2 <- countRecords() + } yield { + v shouldBe defined + v.value.verdictResult shouldBe DbScanVerdictStore.VerdictResultDbValue.Rejected + v.value.recordTime shouldBe ts1 + // The accept's activity record was dropped along with the verdict + countAfterBatch2 shouldBe countAfterBatch1 + } + } } "earliestRoundWithCompleteAppActivity" should { @@ -1153,6 +1248,7 @@ class DbAppActivityRecordStoreTest updateHistory, versions, isFirstSv, + initialRound = 0L, loggerFactory, ) (store, updateHistory.historyId) @@ -1184,6 +1280,7 @@ class DbAppActivityRecordStoreTest updateHistory, DbAppActivityRecordStore.IngestionVersions(1, 0), isFirstSv, + initialRound = 0L, loggerFactory, ) val verdictStore = new DbScanVerdictStore( @@ -1200,6 +1297,7 @@ class DbAppActivityRecordStoreTest verdictStore: DbScanVerdictStore, updateId: String, recordTs: CantonTimestamp, + verdictResult: Short = DbScanVerdictStore.VerdictResultDbValue.Accepted, ): verdictStore.VerdictT = new verdictStore.VerdictT( rowId = 0L, @@ -1208,7 +1306,7 @@ class DbAppActivityRecordStoreTest recordTime = recordTs, finalizationTime = recordTs, submittingParticipantUid = "participant1", - verdictResult = DbScanVerdictStore.VerdictResultDbValue.Accepted, + verdictResult = verdictResult, mediatorGroup = 0, updateId = updateId, submittingParties = Seq.empty, diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala index eabc35301b6..55c6fc8ffb7 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/DbScanAppRewardsStoreTest.scala @@ -1225,6 +1225,7 @@ class DbScanAppRewardsStoreTest updateHistory, DbAppActivityRecordStore.IngestionVersions(1, 0), isFirstSv, + initialRound = 0L, loggerFactory, ) val store = new DbScanAppRewardsStore( diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala index f066410b23f..ae68fe518f6 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/scan/store/ScanEventStoreTest.scala @@ -86,17 +86,20 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl for { ctx <- newEventStore() _ <- insertVerdict(ctx.verdictStore, "update1", CantonTimestamp.MinValue.plusSeconds(1L)) - _ <- ctx.verdictStore.insertVerdictAndTransactionViews( - Seq( - // won't be reinserted - mkVerdict(ctx.verdictStore, "update1", CantonTimestamp.MinValue.plusSeconds(1L)) -> ( - (_: Long) => Seq.empty - ), - // will be inserted - mkVerdict(ctx.verdictStore, "update2", CantonTimestamp.MinValue.plusSeconds(2L)) -> ( - (_: Long) => Seq.empty - ), - ) + _ <- loggerFactory.assertLogs( + ctx.verdictStore.insertVerdictAndTransactionViews( + Seq( + // won't be reinserted + mkVerdict(ctx.verdictStore, "update1", CantonTimestamp.MinValue.plusSeconds(1L)) -> ( + (_: Long) => Seq.empty + ), + // will be inserted + mkVerdict(ctx.verdictStore, "update2", CantonTimestamp.MinValue.plusSeconds(2L)) -> ( + (_: Long) => Seq.empty + ), + ) + ), + _.warningMessage should startWith("Dropping duplicate accepted verdicts"), ) result <- ctx.verdictStore.listVerdicts(None, includeImportUpdates = false, 10) } yield { @@ -894,6 +897,7 @@ class ScanEventStoreTest extends StoreTestBase with HasExecutionContext with Spl updateHistory, DbAppActivityRecordStore.IngestionVersions(1, 0), false, + initialRound = 0L, loggerFactory, ), loggerFactory, diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala index 0e2ad850862..4df89985e30 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala @@ -542,6 +542,7 @@ class DbScanRewardsReferenceStoreTest Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), ), Collections.emptyMap(), true, 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 3d2271e7862..8c25297b67d 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 @@ -1606,6 +1606,7 @@ trait AmuletTransferUtil { self: StoreTestBase => Optional.empty(), Optional.empty(), // voteCooldownTime Optional.empty(), // nextScheduledLogicalSynchronizerUpgrade + Optional.empty(), // svOperationsSwitchOverTimes ), Collections.emptyMap(), true, diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-details-rate-limit.test.tsx b/apps/sv/frontend/src/__tests__/governance/proposal-details-rate-limit.test.tsx new file mode 100644 index 00000000000..906c3068273 --- /dev/null +++ b/apps/sv/frontend/src/__tests__/governance/proposal-details-rate-limit.test.tsx @@ -0,0 +1,79 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { render, screen, within } from '@testing-library/react'; +import userEvent, { UserEvent } from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; +import { afterEach, describe, expect, test } from 'vitest'; + +import App from '../../App'; +import { SvConfigProvider } from '../../utils'; +import { voteRequests } from '../mocks/constants'; +import { server, svUrl } from '../setup/setup'; + +// The SV app's per-client-IP rate limiter answers a burst of requests with a 429 and this body. +const tooManyRequests = () => + new HttpResponse('Too Many Requests: Server is busy, please try again later.', { + status: 429, + }); + +const contractId = voteRequests.dso_rules_vote_requests[0].contract_id; + +// Retries back off exponentially starting at one second, so give the page a few seconds. +const RETRY_TIMEOUT = { timeout: 5000 }; + +const loginAndOpenDetailsPage = async (): Promise => { + const user = userEvent.setup(); + render( + + + + ); + expect(await screen.findByText('Log In')).toBeInTheDocument(); + await user.type(screen.getByRole('textbox'), 'sv1'); + user.click(screen.getByRole('button', { name: 'Log In' })); + expect(await screen.findByTestId('navlink-governance', {}, RETRY_TIMEOUT)).toBeInTheDocument(); + + window.history.pushState({}, '', `/governance/proposals/${contractId}`); + window.dispatchEvent(new PopStateEvent('popstate')); + return user; +}; + +describe('proposal details under rate limiting', () => { + afterEach(() => { + // Start every test logged out and on the landing page. + window.sessionStorage.clear(); + window.history.pushState({}, '', '/'); + }); + + test('renders the proposal when nothing is rate limited', async () => { + await loginAndOpenDetailsPage(); + + expect( + await screen.findByTestId('proposal-details-title', {}, RETRY_TIMEOUT) + ).toHaveTextContent('Proposal Details'); + }); + + test('renders the proposal after the lookup was rate limited once', async () => { + server.use(http.get(`${svUrl}/v0/admin/sv/voterequests/:id`, tooManyRequests, { once: true })); + + await loginAndOpenDetailsPage(); + + expect( + await screen.findByTestId('proposal-details-title', {}, RETRY_TIMEOUT) + ).toHaveTextContent('Proposal Details'); + expect(screen.queryByText(/Unable to find the proposal/)).not.toBeInTheDocument(); + }); + + test('shows the success message after casting the vote was rate limited once', async () => { + server.use(http.post(`${svUrl}/v0/admin/sv/votes`, tooManyRequests, { once: true })); + + const user = await loginAndOpenDetailsPage(); + const votingForm = await screen.findByTestId('your-vote-form', {}, RETRY_TIMEOUT); + + // Clicking Accept both selects the vote and submits; not awaited so the submitting state is observable. + user.click(within(votingForm).getByTestId('your-vote-accept')); + + const successMessage = await screen.findByTestId('vote-submission-success', {}, RETRY_TIMEOUT); + expect(successMessage.textContent).toMatch(/Vote successfully updated/); + }); +}); diff --git a/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts b/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts index 1035962057e..3a4c38f4a02 100644 --- a/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts +++ b/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts @@ -346,6 +346,65 @@ describe('buildAmuletRulesConfigFromChanges', () => { ]); }); + test('should round-trip the development fund blacklist and minting delay', () => { + const changes: ConfigChange[] = [ + { + fieldName: 'developmentFundManagerBlacklist', + label: 'Development Fund Manager Blacklist', + currentValue: 'alice::122', + newValue: 'alice::122, bob::122', + }, + { + fieldName: 'minDevelopmentFundMintingDelay', + label: 'Min Development Fund Minting Delay', + currentValue: '', + newValue: '604800000000', + }, + ]; + + const result = buildAmuletRulesConfigFromChanges(changes); + + expect(result.developmentFundManagerBlacklist).toEqual(['alice::122', 'bob::122']); + expect(result.minDevelopmentFundMintingDelay).toEqual({ microseconds: '604800000000' }); + }); + + test('should map an emptied development fund blacklist and delay to null', () => { + const changes: ConfigChange[] = [ + { + fieldName: 'developmentFundManagerBlacklist', + label: 'Development Fund Manager Blacklist', + currentValue: 'alice::122', + newValue: ' , ', + }, + { + fieldName: 'minDevelopmentFundMintingDelay', + label: 'Min Development Fund Minting Delay', + currentValue: '604800000000', + newValue: '', + }, + ]; + + const result = buildAmuletRulesConfigFromChanges(changes); + + expect(result.developmentFundManagerBlacklist).toBeNull(); + expect(result.minDevelopmentFundMintingDelay).toBeNull(); + }); + + test('should map an absent development fund blacklist to null', () => { + const changes: ConfigChange[] = [ + { + fieldName: 'minDevelopmentFundMintingDelay', + label: 'Min Development Fund Minting Delay', + currentValue: '', + newValue: '604800000000', + }, + ]; + + const result = buildAmuletRulesConfigFromChanges(changes); + + expect(result.developmentFundManagerBlacklist).toBeNull(); + }); + test('should handle issuance curve future values', () => { const changes: ConfigChange[] = [ { diff --git a/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx b/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx index 8642da06bcb..35a16cdd9b8 100644 --- a/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx +++ b/apps/sv/frontend/src/components/governance/ProposalVoteForm.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useForm } from '@tanstack/react-form'; +import { retryOnRateLimit } from '@canton-network/splice-common-frontend'; import { z } from 'zod'; import { useSvAdminClient } from '../../contexts/SvAdminServiceContext'; import { useMutation, UseMutationResult } from '@tanstack/react-query'; @@ -36,12 +37,13 @@ export const ProposalVoteForm: React.FC = props => { const { castVote } = useSvAdminClient(); const yourVote = votes.find(vote => vote.sv === currentSvPartyId); - const castVoteMutation: UseMutationResult = useMutation({ + const castVoteMutation: UseMutationResult = useMutation({ mutationKey: ['castVote', voteRequestContractId], mutationFn: async ({ accepted, url, reason }) => { return castVote(voteRequestContractId, accepted, url, reason); }, onMutate: () => onSubmissionStart?.(), + retry: retryOnRateLimit, }); const form = useForm({ diff --git a/apps/sv/frontend/src/components/layout/NetworkBanner.tsx b/apps/sv/frontend/src/components/layout/NetworkBanner.tsx index 9564059df72..04a8b11b27a 100644 --- a/apps/sv/frontend/src/components/layout/NetworkBanner.tsx +++ b/apps/sv/frontend/src/components/layout/NetworkBanner.tsx @@ -18,6 +18,10 @@ const NetworkBanner: React.FC = () => { alignItems="center" justifyContent="center" sx={{ + position: 'sticky', + top: 0, + zIndex: theme => theme.zIndex.appBar, + pointerEvents: 'none', backgroundColor: networkInstanceNameColor, color: 'black', height: '50px', diff --git a/apps/sv/frontend/src/hooks/useListVoteRequests.tsx b/apps/sv/frontend/src/hooks/useListVoteRequests.tsx index b3b38f3a448..346ea8e3d54 100644 --- a/apps/sv/frontend/src/hooks/useListVoteRequests.tsx +++ b/apps/sv/frontend/src/hooks/useListVoteRequests.tsx @@ -13,6 +13,7 @@ import { useEffect, useRef } from 'react'; import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; import { useConfigPollInterval } from '../utils'; import { shouldContinueVoteHistorySearch } from '../utils/proposalSearch'; +import { retryOnRateLimit, retryQuery } from '@canton-network/splice-common-frontend'; const PAGINATED_VOTE_RESULTS_QUERY_KEY = 'paginatedVoteRequestResults'; const PAGINATED_VOTE_RESULTS_PAGE_SIZE = 500; @@ -69,7 +70,7 @@ export const useListVoteRequestResult = ( ); return List(DsoRules_CloseVoteRequestResult).decoder.runWithException(dso_rules_vote_results); }, - retry, + retry: retry ? retryQuery : retryOnRateLimit, }); }; diff --git a/apps/sv/frontend/src/hooks/usePreviousSvRewardWeight.tsx b/apps/sv/frontend/src/hooks/usePreviousSvRewardWeight.tsx index 026a3781fda..aca50154be7 100644 --- a/apps/sv/frontend/src/hooks/usePreviousSvRewardWeight.tsx +++ b/apps/sv/frontend/src/hooks/usePreviousSvRewardWeight.tsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; +import { retryOnRateLimit } from '@canton-network/splice-common-frontend'; export interface PreviousSvRewardWeightResult { weight: string | undefined; @@ -22,7 +23,7 @@ export function usePreviousSvRewardWeight( return rewardWeight ?? null; }, enabled: !!svParty, - retry: false, + retry: retryOnRateLimit, }); return { weight: query.data ?? undefined, isPending: !!svParty && query.isPending }; diff --git a/apps/sv/frontend/src/hooks/useVoteRequest.tsx b/apps/sv/frontend/src/hooks/useVoteRequest.tsx index e5358a00bf4..a06c67357da 100644 --- a/apps/sv/frontend/src/hooks/useVoteRequest.tsx +++ b/apps/sv/frontend/src/hooks/useVoteRequest.tsx @@ -8,6 +8,7 @@ import { ContractId } from '@daml/types'; import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; import { useConfigPollInterval } from '../utils'; +import { retryOnRateLimit, retryQuery } from '@canton-network/splice-common-frontend'; export const useVoteRequest = ( contractId: ContractId, @@ -23,7 +24,7 @@ export const useVoteRequest = ( const request = await lookupDsoRulesVoteRequest(contractId); return Contract.decodeOpenAPI(request.dso_rules_vote_request, VoteRequest); }, - retry, + retry: retry ? retryQuery : retryOnRateLimit, refetchInterval: poll ? pollInterval : PollingStrategy.NONE, }); }; diff --git a/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts b/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts index c7b3994d66d..528ec761783 100644 --- a/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts +++ b/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts @@ -46,6 +46,19 @@ export function buildAmuletConfigChanges( currentValue: before?.optDevelopmentFundManager || '', newValue: after?.optDevelopmentFundManager || '', }, + { + fieldName: 'developmentFundManagerBlacklist', + label: 'Blacklisted development fund managers (comma-separated party ids)', + currentValue: before?.developmentFundManagerBlacklist?.join(', ') || '', + newValue: after?.developmentFundManagerBlacklist?.join(', ') || '', + }, + { + fieldName: 'minDevelopmentFundMintingDelay', + label: + 'Minimum delay between allocating and minting a development fund coupon in microseconds', + currentValue: before?.minDevelopmentFundMintingDelay?.microseconds || '', + newValue: after?.minDevelopmentFundMintingDelay?.microseconds || '', + }, { fieldName: 'transferConfigCreateFee', label: 'Fee per created output contract in a transfer', diff --git a/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts b/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts index 8c81c2cee81..6c1cb30b0fc 100644 --- a/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts +++ b/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts @@ -93,6 +93,16 @@ export function buildAmuletRulesConfigFromChanges( true ); const transferConfigTokenStandardMaxTTL = getValue('transferConfigTokenStandardMaxTTL', true); + const developmentFundManagerBlacklistParties = + getValue('developmentFundManagerBlacklist', true) + ?.split(',') + .map(party => party.trim()) + .filter(party => party !== '') ?? []; + const developmentFundManagerBlacklist = + developmentFundManagerBlacklistParties.length > 0 + ? developmentFundManagerBlacklistParties + : null; + const minDevelopmentFundMintingDelay = getValue('minDevelopmentFundMintingDelay', true); const rewardConfigMintingVersion = getValue('rewardConfigMintingVersion', true); const amuletConfig: AmuletConfig<'USD'> = { tickDuration: { microseconds: getValue('tickDuration', false) }, @@ -104,6 +114,12 @@ export function buildAmuletRulesConfigFromChanges( ? null : { microseconds: externalPartyConfigStateTickDuration }, transferPreapprovalBaseDuration: null, + developmentFundManagerBlacklist, + minDevelopmentFundMintingDelay: + minDevelopmentFundMintingDelay === null + ? null + : { microseconds: minDevelopmentFundMintingDelay }, + amuletSwitchOverTimes: null, transferConfig: { createFee: { fee: getValue('transferConfigCreateFee', false) }, holdingFee: { rate: getValue('transferConfigHoldingFeeRate', false) }, diff --git a/apps/sv/frontend/src/utils/buildDsoRulesConfigFromChanges.ts b/apps/sv/frontend/src/utils/buildDsoRulesConfigFromChanges.ts index 6ede0752923..2a229de1fb5 100644 --- a/apps/sv/frontend/src/utils/buildDsoRulesConfigFromChanges.ts +++ b/apps/sv/frontend/src/utils/buildDsoRulesConfigFromChanges.ts @@ -120,6 +120,7 @@ export function buildDsoRulesConfigFromChanges(dsoConfigChanges: ConfigChange[]) ), }, voteCooldownTime: voteCooldownTime === null ? null : { microseconds: voteCooldownTime }, + svOperationsSwitchOverTimes: null, }; return dsoConfig; diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala index ddcbfa2bff9..460795b77de 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/FeaturedAppActivityMarkerTrigger.scala @@ -112,7 +112,6 @@ class FeaturedAppActivityMarkerTrigger( ignorePartiesWithoutVettedAmulet( getInformeesFromContracts(markers.flatten), markers.flatten.map(_.contractId.contractId), - logAsWarning = true, ).discard Seq.empty } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala index b6f001e9b0a..eead08dcf95 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/delegatebased/IgnoredUnavailablePartiesGuard.scala @@ -67,14 +67,10 @@ trait IgnoredUnavailablePartiesGuard extends NamedLogging { protected def ignorePartiesWithoutVettedAmulet( informees: Set[PartyId], contractIds: Seq[String], - logAsWarning: Boolean = false, - )(implicit tc: TraceContext): String = { + ): String = { val toIgnore = withoutDsoParty(informees) ignoredPartiesStore.addAll(toIgnore) - val msg = - s"No vetted Amulet version for $contractIds; ignoring ${toIgnore.size} parties: $toIgnore" - if (logAsWarning) logger.warn(msg) - msg + s"No vetted Amulet version for $contractIds; ignoring ${toIgnore.size} parties: $toIgnore" } private def recoverUnresponsiveParties( diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala index 65cac6b4cb8..3f987b521b7 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala @@ -21,6 +21,7 @@ import com.digitalasset.canton.config.RequireTypes.{ PositiveLong, PositiveNumeric, } +import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.synchronizer.mediator.RemoteMediatorConfig import com.digitalasset.canton.synchronizer.sequencer.config.RemoteSequencerConfig import com.digitalasset.canton.topology.PartyId @@ -52,7 +53,7 @@ import org.lfdecentralizedtrust.splice.lsu.LsuRollForwardTimestamp import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig import org.lfdecentralizedtrust.splice.sv.SvAppClientConfig import org.lfdecentralizedtrust.splice.sv.util.SvUtil -import org.lfdecentralizedtrust.splice.util.SpliceUtil +import org.lfdecentralizedtrust.splice.util.{SpliceUtil, SwitchOverTimes} import java.nio.file.Path @@ -121,7 +122,15 @@ object SvOnboardingConfig { developmentFundManager: Option[PartyId] = None, initialExternalPartyConfigStateTickDuration: Option[NonNegativeFiniteDuration] = None, optValidatorFaucetCap: Option[BigDecimal] = None, - initialRewardConfig: Option[InitialRewardConfig] = None, + // Networks default to FeaturedAppMarkers minting with TrafficBasedAppRewards + // dry-run alongside. Tests default to TrafficBasedAppRewards minting (no + // dry-run) via a config transform in ConfigTransforms.defaults(). + initialRewardConfig: Option[InitialRewardConfig] = Some(InitialRewardConfig()), + initialSvOperationsSwitchOverTimes: Option[Map[String, CantonTimestamp]] = Some( + Map( + SwitchOverTimes.NoFeaturedAppChoiceContext -> CantonTimestamp.MinValue + ) + ), ) extends SvOnboardingConfig case class JoinWithKey( @@ -249,7 +258,7 @@ object SvOnboardingConfig { final case class InitialRewardConfig( mintingVersion: String = "RewardVersion_FeaturedAppMarkers", - dryRunVersion: Option[String] = None, + dryRunVersion: Option[String] = Some("RewardVersion_TrafficBasedAppRewards"), batchSize: Long = 100, rewardCouponTimeToLiveMicros: Long = 36L * 60 * 60 * 1000000, // 36 hours appRewardCouponThreshold: BigDecimal = BigDecimal("0.5"), diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala index 8cc1f727d70..f8edfcc0bdb 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/sv1/SV1Initializer.scala @@ -641,6 +641,8 @@ class SV1Initializer( synchronizerId, sv1Config.voteCooldownTime, sv1Config.acsCommitmentReconciliationInterval, + sv1Config.initialSvOperationsSwitchOverTimes, + sv1Config.initialPackageConfig, ) for { (participantId, trafficStateForAllMembers, amuletRules, dsoRules) <- ( diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala index 301d1d387f4..0c1d155aaad 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala @@ -7,6 +7,7 @@ import cats.data.OptionT import cats.implicits.* import com.daml.ledger.javaapi.data as javab import com.daml.ledger.javaapi.data.codegen.ContractId +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.* @@ -923,48 +924,52 @@ class DbSvDsoStore( ]], ) ] = { - if (rounds.isEmpty) - Future.successful((Seq.empty, Seq.empty)) - else { - val roundsClause = inClause("mining_round", rounds) - val calculateRewardsF = storage - .query( - selectFromAcsTableWithState( - DsoTables.acsTableName, - acsStoreId, - domainMigrationId, - splice.amulet.rewardaccountingv2.CalculateRewardsV2.COMPANION, - additionalWhere = (sql" and " ++ roundsClause).toActionBuilder, - ), - "listDryRunCalculateRewardsV2ByRounds", - ) - .map( - _.map( - assignedContractFromRow(splice.amulet.rewardaccountingv2.CalculateRewardsV2.COMPANION)( - _ - ) - ).filter(_.payload.dryRun) - ) - val processRewardsF = storage - .query( - selectFromAcsTableWithState( - DsoTables.acsTableName, - acsStoreId, - domainMigrationId, - splice.amulet.rewardaccountingv2.ProcessRewardsV2.COMPANION, - additionalWhere = (sql" and " ++ roundsClause).toActionBuilder, - ), - "listDryRunProcessRewardsV2ByRounds", - ) - .map( - _.map( - assignedContractFromRow(splice.amulet.rewardaccountingv2.ProcessRewardsV2.COMPANION)(_) - ).filter(_.payload.dryRun) - ) - for { - calculateRewards <- calculateRewardsF - processRewards <- processRewardsF - } yield (calculateRewards, processRewards) + NonEmpty.from(rounds) match { + case None => Future.successful((Seq.empty, Seq.empty)) + case Some(rounds) => + val roundsClause = DbStorage.toInClause("mining_round", rounds) + val calculateRewardsF = storage + .query( + selectFromAcsTableWithState( + DsoTables.acsTableName, + acsStoreId, + domainMigrationId, + splice.amulet.rewardaccountingv2.CalculateRewardsV2.COMPANION, + additionalWhere = (sql" and " ++ roundsClause).toActionBuilder, + ), + "listDryRunCalculateRewardsV2ByRounds", + ) + .map( + _.map( + assignedContractFromRow( + splice.amulet.rewardaccountingv2.CalculateRewardsV2.COMPANION + )( + _ + ) + ).filter(_.payload.dryRun) + ) + val processRewardsF = storage + .query( + selectFromAcsTableWithState( + DsoTables.acsTableName, + acsStoreId, + domainMigrationId, + splice.amulet.rewardaccountingv2.ProcessRewardsV2.COMPANION, + additionalWhere = (sql" and " ++ roundsClause).toActionBuilder, + ), + "listDryRunProcessRewardsV2ByRounds", + ) + .map( + _.map( + assignedContractFromRow(splice.amulet.rewardaccountingv2.ProcessRewardsV2.COMPANION)( + _ + ) + ).filter(_.payload.dryRun) + ) + for { + calculateRewards <- calculateRewardsF + processRewards <- processRewardsF + } yield (calculateRewards, processRewards) } } @@ -1563,29 +1568,33 @@ class DbSvDsoStore( tc: TraceContext ): Future[Seq[Contract[AmuletPriceVote.ContractId, AmuletPriceVote]]] = waitUntilAcsIngested { import scala.jdk.CollectionConverters.* - for { - dsoRules <- getDsoRules() - voterParties = inClause( - "voter", - dsoRules.payload.svs.asScala - .map { case (party, _) => - lengthLimited(party) - }, - ) - result <- storage - .query( - selectFromAcsTable( - DsoTables.acsTableName, - acsStoreId, - domainMigrationId, - AmuletPriceVote.COMPANION, - where = voterParties, - orderLimit = sql"""limit ${sqlLimit(limit)}""", - ), - "listSvAmuletPriceVotes", - ) - limited = applyLimit("listSvAmuletPriceVotes", limit, result) - } yield limited.map(contractFromRow(AmuletPriceVote.COMPANION)(_)).distinctBy(_.payload.sv) + getDsoRules().flatMap(dsoRules => + NonEmpty.from( + dsoRules.payload.svs.asScala.toMap.map { case (party, _) => lengthLimited(party) } + ) match { + case None => Future.successful(Seq.empty) + case Some(voters) => + for { + dsoRules <- getDsoRules() + voterParties = DbStorage.toInClause("voter", voters) + result <- storage + .query( + selectFromAcsTable( + DsoTables.acsTableName, + acsStoreId, + domainMigrationId, + AmuletPriceVote.COMPANION, + where = voterParties, + orderLimit = sql"""limit ${sqlLimit(limit)}""", + ), + "listSvAmuletPriceVotes", + ) + limited = applyLimit("listSvAmuletPriceVotes", limit, result) + } yield limited + .map(contractFromRow(AmuletPriceVote.COMPANION)(_)) + .distinctBy(_.payload.sv) + } + ) } override protected def lookupSvOnboardingRequestByCandidatePartyWithOffset( @@ -1705,22 +1714,26 @@ class DbSvDsoStore( )(implicit tc: TraceContext ): Future[Seq[Contract[VoteRequest.ContractId, VoteRequest]]] = waitUntilAcsIngested { - for { - result <- storage - .query( - listVoteRequestsByTrackingCidQuery( - acsTableName = DsoTables.acsTableName, - acsStoreId = acsStoreId, - domainMigrationId = domainMigrationId, - trackingCidColumnName = "vote_request_tracking_cid", - trackingCids = trackingCids, - limit = limit, - ), - "listVoteRequestsByTrackingCid", - ) - records = applyLimit("listVoteRequestsByTrackingCid", limit, result) - } yield records - .map(contractFromRow(VoteRequest.COMPANION)(_)) + NonEmpty.from(trackingCids) match { + case None => Future.successful(Seq.empty) + case Some(trackingCids) => + for { + result <- storage + .query( + listVoteRequestsByTrackingCidQuery( + acsTableName = DsoTables.acsTableName, + acsStoreId = acsStoreId, + domainMigrationId = domainMigrationId, + trackingCidColumnName = "vote_request_tracking_cid", + trackingCids = trackingCids, + limit = limit, + ), + "listVoteRequestsByTrackingCid", + ) + records = applyLimit("listVoteRequestsByTrackingCid", limit, result) + } yield records + .map(contractFromRow(VoteRequest.COMPANION)(_)) + } } override def lookupVoteByThisSvAndVoteRequestWithOffset(voteRequestCid: VoteRequest.ContractId)( @@ -1959,27 +1972,27 @@ class DbSvDsoStore( )(implicit tc: TraceContext): Future[ Seq[Contract[splice.round.ClosedMiningRound.ContractId, splice.round.ClosedMiningRound]] ] = { - if (roundNumbers.isEmpty) - Future.successful(Seq.empty) - else { - val roundNumbersClause = inClause("mining_round", roundNumbers) - waitUntilAcsIngested { - for { - result <- storage - .query( - selectFromAcsTable( - DsoTables.acsTableName, - acsStoreId, - domainMigrationId, - ClosedMiningRound.COMPANION, - where = - (sql"""assigned_domain = $synchronizerId AND """ ++ roundNumbersClause).toActionBuilder, - orderLimit = sql"""limit ${sqlLimit(limit)}""", - ), - "listClosedRounds", - ) - } yield result.map(contractFromRow(ClosedMiningRound.COMPANION)(_)) - } + NonEmpty.from(roundNumbers) match { + case None => Future.successful(Seq.empty) + case Some(roundNumbers) => + val roundNumbersClause = DbStorage.toInClause("mining_round", roundNumbers) + waitUntilAcsIngested { + for { + result <- storage + .query( + selectFromAcsTable( + DsoTables.acsTableName, + acsStoreId, + domainMigrationId, + ClosedMiningRound.COMPANION, + where = + (sql"""assigned_domain = $synchronizerId AND """ ++ roundNumbersClause).toActionBuilder, + orderLimit = sql"""limit ${sqlLimit(limit)}""", + ), + "listClosedRounds", + ) + } yield result.map(contractFromRow(ClosedMiningRound.COMPANION)(_)) + } } } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/SvUtil.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/SvUtil.scala index 42ed608cf63..e0aeccae2b9 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/SvUtil.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/util/SvUtil.scala @@ -21,16 +21,23 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.decentralizedsync import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.DsoRulesConfig import org.lfdecentralizedtrust.splice.codegen.java.splice.{cometbft, dso} import org.lfdecentralizedtrust.splice.codegen.java.da.time.types.RelTime +import org.lfdecentralizedtrust.splice.environment.DarResources import org.lfdecentralizedtrust.splice.sv.{LocalSynchronizerNode, SvSynchronizerNode} import org.lfdecentralizedtrust.splice.sv.cometbft.CometBftNode -import org.lfdecentralizedtrust.splice.sv.config.{BeneficiaryConfig, SvScanConfig} +import org.lfdecentralizedtrust.splice.sv.config.{ + BeneficiaryConfig, + SvScanConfig, + SvOnboardingConfig, +} import com.digitalasset.canton.config.RequireTypes.PositiveInt import com.digitalasset.canton.config.{NonNegativeFiniteDuration, PositiveDurationSeconds} +import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.logging.TracedLogger import com.digitalasset.canton.protocol.AcsCommitmentsCatchUpParameters import com.digitalasset.canton.time.Clock import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext +import com.digitalasset.daml.lf.data.Ref.PackageVersion import java.security.interfaces.{ECPrivateKey, ECPublicKey} import java.security.spec.{EncodedKeySpec, PKCS8EncodedKeySpec, X509EncodedKeySpec} @@ -241,24 +248,37 @@ object SvUtil { synchronizerId: SynchronizerId, voteCooldownTime: Option[NonNegativeFiniteDuration] = None, acsCommitmentReconciliationInterval: PositiveDurationSeconds, - ): DsoRulesConfig = new DsoRulesConfig( - 10, // numUnclaimedRewardsThreshold - 5, // numMemberTrafficContractsThreshold, arbitrarily set as 5 for now. - new RelTime(TimeUnit.HOURS.toMicros(1)), // actionConfirmationTimeout - new RelTime(TimeUnit.HOURS.toMicros(1)), // svOnboardingRequestTimeout - new RelTime(TimeUnit.HOURS.toMicros(1)), // svOnboardingConfirmedTimeout - new RelTime(TimeUnit.HOURS.toMicros(7 * 24)), // voteRequestTimeout - new RelTime(TimeUnit.SECONDS.toMicros(70)), // dsoDelegateInactiveTimeout - defaultSynchronizerNodeConfigLimits, - 1024, // maxTextLength - defaultDsoDecentralizedSynchronizerConfig( - synchronizerId, - acsCommitmentReconciliationInterval, - ), // decentralizedSynchronizerConfig - Optional.empty(), // nextScheduledSynchronizerUpgrade - voteCooldownTime.map(t => new RelTime(t.duration.toMicros)).toJava, - Optional.empty(), // nextScheduledLogicalSynchronizerUpgrade - ) + switchOverTimes: Option[Map[String, CantonTimestamp]], + packageConfig: SvOnboardingConfig.InitialPackageConfig, + ): DsoRulesConfig = { + // This runs too early to do a proper topology version check so just check the config here. + val supportsSwitchoverTimes = PackageVersion.assertFromString( + packageConfig.dsoGovernanceVersion + ) >= DarResources.dsoGovernance_0_1_29.metadata.version + new DsoRulesConfig( + 10, // numUnclaimedRewardsThreshold + 5, // numMemberTrafficContractsThreshold, arbitrarily set as 5 for now. + new RelTime(TimeUnit.HOURS.toMicros(1)), // actionConfirmationTimeout + new RelTime(TimeUnit.HOURS.toMicros(1)), // svOnboardingRequestTimeout + new RelTime(TimeUnit.HOURS.toMicros(1)), // svOnboardingConfirmedTimeout + new RelTime(TimeUnit.HOURS.toMicros(7 * 24)), // voteRequestTimeout + new RelTime(TimeUnit.SECONDS.toMicros(70)), // dsoDelegateInactiveTimeout + defaultSynchronizerNodeConfigLimits, + 1024, // maxTextLength + defaultDsoDecentralizedSynchronizerConfig( + synchronizerId, + acsCommitmentReconciliationInterval, + ), // decentralizedSynchronizerConfig + Optional.empty(), // nextScheduledSynchronizerUpgrade + voteCooldownTime.map(t => new RelTime(t.duration.toMicros)).toJava, + Optional.empty(), // nextScheduledLogicalSynchronizerUpgrade + // We silently drop all switchover values when switchover is not supported as making it an error doesn't work well with setting it as the default. + switchOverTimes + .map(_.view.mapValues(_.toInstant).toMap.asJava) + .filter(_ => supportsSwitchoverTimes) + .toJava, + ) + } def keyPairMatches( publicKeyBase64: String, diff --git a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala index aeb5808050a..07d2f850c7a 100644 --- a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala +++ b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala @@ -2164,6 +2164,7 @@ abstract class SvDsoStoreTest extends StoreTestBase with HasExecutionContext { Optional.empty(), Optional.empty(), // voteCooldownTime` Optional.empty(), // nextScheduledLogicalSynchronizerUpgrade` + Optional.empty(), // svOperationsSwitchOverTimes ), Collections.emptyMap(), true, diff --git a/apps/validator/src/main/openapi/validator-internal.yaml b/apps/validator/src/main/openapi/validator-internal.yaml index b68fe6ca103..9d07fd1cd77 100644 --- a/apps/validator/src/main/openapi/validator-internal.yaml +++ b/apps/validator/src/main/openapi/validator-internal.yaml @@ -224,71 +224,6 @@ paths: schema: "$ref": "#/components/schemas/ListTransferPreapprovalsResponse" - /v0/admin/external-party/transfer-preapproval/prepare-send: - post: - tags: [validator] - deprecated: true - x-jvm-package: validator_admin - operationId: "prepareTransferPreapprovalSend" - description: | - **Deprecated**. Prepare a transaction to create a TransferCommand with the given CC amount to the specified receiver - from the externally hosted sender. - The transaction then needs to be signed and submitted through - /v0/admin/external-party/transfer-preapproval/submit-send. - security: - - adminAuth: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/PrepareTransferPreapprovalSendRequest" - responses: - "200": - description: ok - content: - application/json: - schema: - "$ref": "#/components/schemas/PrepareTransferPreapprovalSendResponse" - "400": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" - "404": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" - "501": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/501" - - /v0/admin/external-party/transfer-preapproval/submit-send: - post: - tags: [validator] - deprecated: true - x-jvm-package: validator_admin - operationId: "submitTransferPreapprovalSend" - description: | - **Deprecated**. Submit transaction generated by /v0/admin/transfer-preapproval/prepare-send - together with its signature. Note that this only waits until the TransferCommand is created. - The actual transfer will happen afterwards through automation run by the SVs. - security: - - adminAuth: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/SubmitTransferPreapprovalSendRequest" - responses: - "200": - description: ok - content: - application/json: - schema: - "$ref": "#/components/schemas/SubmitTransferPreapprovalSendResponse" - "400": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" - "404": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" - "501": - $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/501" - /v0/admin/external-party/topology/generate: post: tags: [ validator ] diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala index f9b91735502..89f20c2d631 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala @@ -782,7 +782,6 @@ class ValidatorApp( config.parameters.enabledFeatures, config.additionalPackagesToUnvet, config.domains.global.alias, - config.enableDeprecatedTransferCommandSupport, loggerFactory, packageVersionSupport, ) @@ -866,7 +865,6 @@ class ValidatorApp( getAmuletRulesDomain = scanConnection.getAmuletRulesDomain, scanConnection = scanConnection, participantAdminConnection, - packageVersionSupport, config, clock, retryProvider = retryProvider, diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpValidatorAdminAppClient.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpValidatorAdminAppClient.scala index c5deff749aa..db45622ad8f 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpValidatorAdminAppClient.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpValidatorAdminAppClient.scala @@ -17,11 +17,9 @@ import org.lfdecentralizedtrust.splice.util.{ ContractWithState, TemplateJsonDecoder, } -import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.topology.{ParticipantId, PartyId, SynchronizerId} import org.apache.pekko.http.scaladsl.model.{HttpHeader, HttpResponse, StatusCodes} -import java.time.ZoneOffset import scala.concurrent.Future object HttpValidatorAdminAppClient { @@ -425,79 +423,4 @@ object HttpValidatorAdminAppClient { Right(response) } } - - case class PrepareTransferPreapprovalSend( - senderPartyId: PartyId, - receiverPartyId: PartyId, - amount: BigDecimal, - expiresAt: CantonTimestamp, - nonce: Long, - description: Option[String], - verboseHashing: Boolean, - ) extends BaseCommand[ - http.PrepareTransferPreapprovalSendResponse, - definitions.PrepareTransferPreapprovalSendResponse, - ] { - - override def submitRequest( - client: Client, - headers: List[HttpHeader], - ): EitherT[Future, Either[ - Throwable, - HttpResponse, - ], http.PrepareTransferPreapprovalSendResponse] = - client.prepareTransferPreapprovalSend( - definitions.PrepareTransferPreapprovalSendRequest( - senderPartyId.toProtoPrimitive, - receiverPartyId.toProtoPrimitive, - amount, - expiresAt.toInstant.atOffset(ZoneOffset.UTC), - nonce, - Some(verboseHashing), - description = description, - ), - headers = headers, - ) - - override protected def handleOk()(implicit - decoder: TemplateJsonDecoder - ) = { case http.PrepareTransferPreapprovalSendResponse.OK(response) => - Right(response) - } - } - - case class SubmitTransferPreapprovalSend( - senderPartyId: PartyId, - transaction: String, - signature: String, - publicKey: String, - ) extends BaseCommand[ - http.SubmitTransferPreapprovalSendResponse, - String, - ] { - - override def submitRequest( - client: Client, - headers: List[HttpHeader], - ): EitherT[Future, Either[ - Throwable, - HttpResponse, - ], http.SubmitTransferPreapprovalSendResponse] = - client.submitTransferPreapprovalSend( - definitions.SubmitTransferPreapprovalSendRequest( - definitions.ExternalPartySubmission( - senderPartyId.toProtoPrimitive, - transaction, - signature, - publicKey, - ) - ), - headers = headers, - ) - - override protected def handleOk()(implicit - decoder: TemplateJsonDecoder - ) = { case http.SubmitTransferPreapprovalSendResponse.OK(response) => Right(response.updateId) } - } - } diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala index 8f9bb3b47d3..1be5c53f18f 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala @@ -5,7 +5,6 @@ package org.lfdecentralizedtrust.splice.validator.admin.http import cats.implicits.catsSyntaxOptionId import cats.syntax.either.* -import com.daml.ledger.api.v2.interactive import org.lfdecentralizedtrust.splice.admin.http.HttpErrorHandler import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.{Amulet, LockedAmulet} import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{ @@ -18,7 +17,6 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.install.amulet import org.lfdecentralizedtrust.splice.environment.ledger.api.DedupOffset import org.lfdecentralizedtrust.splice.environment.{ BaseLedgerConnection, - PackageVersionSupport, ParticipantAdminConnection, RetryFor, RetryProvider, @@ -61,7 +59,6 @@ import java.util.Base64 import scala.annotation.nowarn import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.* -import scala.jdk.OptionConverters.* class HttpValidatorAdminHandler( storeWithIngestion: AppStoreWithIngestion[ValidatorStore], @@ -72,7 +69,6 @@ class HttpValidatorAdminHandler( getAmuletRulesDomain: GetAmuletRulesDomain, scanConnection: ScanConnection, participantAdminConnection: ParticipantAdminConnection, - packageVersionSupport: PackageVersionSupport, config: ValidatorAppBackendConfig, clock: Clock, retryProvider: RetryProvider, @@ -96,16 +92,6 @@ class HttpValidatorAdminHandler( )(handleRequest) } - private def requireTransferCommandSupport[T](handleRequest: => T): T = { - if (config.enableDeprecatedTransferCommandSupport) { - handleRequest - } else { - throw HttpErrorHandler.notImplemented( - "Transfer command support is disabled by default and will be removed in 0.8.0. You can temporarily enable in on 0.7.x by setting enable-deprecated-transfer-command-support=true in your validator config." - ) - } - } - def onboardUser( respond: v0.ValidatorAdminResource.OnboardUserResponse.type )( @@ -708,123 +694,6 @@ class HttpValidatorAdminHandler( ) } - override def prepareTransferPreapprovalSend( - respond: v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse.type - )(body: definitions.PrepareTransferPreapprovalSendRequest)( - tuser: AdminUserRequest - ): Future[v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse] = { - implicit val AdminUserRequest(tracedContext) = tuser - requireWalletEnabled { _ => - requireTransferCommandSupport { - val senderParty = PartyId.tryFromProtoPrimitive(body.senderPartyId) - val receiverParty = PartyId.tryFromProtoPrimitive(body.receiverPartyId) - for { - synchronizerId <- getAmuletRulesDomain()(tracedContext) - // This check is just to make it fail early. The actual preapproval is fixed when the automation - // executes the transfer but we want the user to get feedback during the prepare step already. - _ <- scanConnection.lookupTransferPreapprovalByParty(receiverParty).map { preapprovalO => - if (preapprovalO.isEmpty) { - throw Status.INVALID_ARGUMENT - .withDescription(s"Receiver $receiverParty does not have a TransferPreapproval") - .asRuntimeException - } - } - externalPartyAmuletRules <- scanConnection.getExternalPartyAmuletRules() - supportsDescription <- packageVersionSupport - .supportsDescriptionInTransferPreapprovals( - Seq(receiverParty, senderParty, store.key.dsoParty), - clock.now, - ) - .map(_.supported) - commands = externalPartyAmuletRules.toAssignedContract - .getOrElse( - throw Status.Code.FAILED_PRECONDITION.toStatus - .withDescription( - s"ExternalPartyAmuletRules is currently inflight between synchronizers, retry until it is assigned to a synchronizer" - ) - .asRuntimeException() - ) - .exercise( - _.exerciseExternalPartyAmuletRules_CreateTransferCommand( - senderParty.toProtoPrimitive, - receiverParty.toProtoPrimitive, - store.key.validatorParty.toProtoPrimitive, - body.amount.bigDecimal, - body.expiresAt.toInstant, - body.nonce, - Option.when(supportsDescription)(body.description).flatten.toJava, - java.util.Optional.of(store.key.dsoParty.toProtoPrimitive), - ) - ) - .update - .commands() - .asScala - .toSeq - r <- storeWithIngestion - .connection(SpliceLedgerConnectionPriority.Medium) - .prepareSubmission( - Some(synchronizerId), - Seq(senderParty), - Seq(senderParty), - commands, - storeWithIngestion - .connection(SpliceLedgerConnectionPriority.Medium) - .disclosedContracts(externalPartyAmuletRules), - body.verboseHashing.getOrElse(false), - ) - transferCommandCid = r.preparedTransaction - .flatMap(_.transaction) - .toList - .flatMap(_.nodes) - .flatMap(n => - n.getV1.nodeType match { - case interactive.transaction.v1.interactive_submission_data.Node.NodeType - .Create(create) => - Seq(create.contractId) - case _ => Seq.empty - } - ) - .headOption - .getOrElse( - throw Status.INTERNAL - .withDescription("Failed to obtain transferCommandCid from prepared transaction") - .asRuntimeException() - ) - } yield { - v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse.OK( - definitions.PrepareTransferPreapprovalSendResponse( - Base64.getEncoder.encodeToString(r.getPreparedTransaction.toByteArray), - HexString.toHexString(r.preparedTransactionHash), - transferCommandCid, - r.hashingDetails, - ) - ) - } - } - } - } - - override def submitTransferPreapprovalSend( - respond: v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponse.type - )(body: definitions.SubmitTransferPreapprovalSendRequest)( - tuser: AdminUserRequest - ): Future[v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponse] = { - implicit val AdminUserRequest(tracedContext) = tuser - requireWalletEnabled { _ => - requireTransferCommandSupport { - for { - updateId <- ValidatorUtil.submitAsExternalParty( - storeWithIngestion.connection(SpliceLedgerConnectionPriority.Medium), - body.submission, - waitForOffset = false, - ) - } yield v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponseOK( - definitions.SubmitTransferPreapprovalSendResponse(updateId) - ) - } - } - } - def getExternalPartyAmulets(partyId: PartyId)(implicit tc: TraceContext): Future[ (Seq[Contract[Amulet.ContractId, Amulet]], Seq[Contract[LockedAmulet.ContractId, LockedAmulet]]) ] = { diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/TransferCommandSendTrigger.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/TransferCommandSendTrigger.scala deleted file mode 100644 index 8aac7325df2..00000000000 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/TransferCommandSendTrigger.scala +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package org.lfdecentralizedtrust.splice.validator.automation - -import cats.syntax.traverse.* -import org.apache.pekko.stream.Materializer -import org.lfdecentralizedtrust.splice.automation.{ - OnAssignedContractTrigger, - TaskOutcome, - TaskSuccess, - TriggerContext, -} -import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.transferinput.InputAppRewardCoupon -import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.{ - PaymentTransferContext, - TransferContext, - TransferInput, -} -import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletrules.{ - TransferCommand -} -import org.lfdecentralizedtrust.splice.codegen.java.splice.round.IssuingMiningRound -import org.lfdecentralizedtrust.splice.codegen.java.splice.types.Round -import org.lfdecentralizedtrust.splice.environment.{RetryFor, SpliceLedgerConnection} -import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection -import org.lfdecentralizedtrust.splice.store.PageLimit -import org.lfdecentralizedtrust.splice.util.{ContractWithState, AssignedContract, SpliceUtil} -import org.lfdecentralizedtrust.splice.validator.store.ValidatorStore -import org.lfdecentralizedtrust.splice.wallet.ExternalPartyWalletManager -import com.digitalasset.canton.data.CantonTimestamp -import com.digitalasset.canton.topology.PartyId -import com.digitalasset.canton.tracing.TraceContext -import io.grpc.Status -import io.opentelemetry.api.trace.Tracer - -import scala.concurrent.{ExecutionContext, Future} -import scala.jdk.CollectionConverters.* -import scala.jdk.OptionConverters.* - -class TransferCommandSendTrigger( - override protected val context: TriggerContext, - scanConnection: BftScanConnection, - store: ValidatorStore, - walletManager: ExternalPartyWalletManager, - ledgerConnection: SpliceLedgerConnection, -)(implicit - override val ec: ExecutionContext, - mat: Materializer, - tracer: Tracer, -) extends OnAssignedContractTrigger.Template[TransferCommand.ContractId, TransferCommand]( - store, - TransferCommand.COMPANION, - ) { - - override def completeTask( - transferCommand: AssignedContract[TransferCommand.ContractId, TransferCommand] - )(implicit tc: TraceContext): Future[TaskOutcome] = { - val now = context.clock.now - val sender = PartyId.tryFromProtoPrimitive(transferCommand.payload.sender) - val receiver = PartyId.tryFromProtoPrimitive(transferCommand.payload.receiver) - if (CantonTimestamp.tryFromInstant(transferCommand.payload.expiresAt) < now) { - Future.successful(TaskSuccess("TransferCommand is expired, skipping")) - } else { - walletManager.lookupExternalPartyWallet(sender) match { - case None => - Future.successful( - TaskSuccess( - s"Sender of transfer command $sender is not an onboarded external party, skipping." - ) - ) - case Some(wallet) => - for { - transferPreapprovalO <- scanConnection.lookupTransferPreapprovalByParty(receiver) - (openRounds, issuingMiningRounds) <- scanConnection.getOpenAndIssuingMiningRounds() - openRound = SpliceUtil.selectLatestOpenMiningRound(now, openRounds) - configUsd = openRound.payload.transferConfigUsd - maxNumInputs = configUsd.maxNumInputs.intValue() - amulets <- wallet.store.listSortedAmuletsAndQuantity( - PageLimit.tryCreate(maxNumInputs) - ) - openIssuingRounds = issuingMiningRounds.filter(c => - c.payload.opensAt.isBefore(now.toInstant) - ) - issuingRoundsMap = openIssuingRounds.view.map { r => - val imr = r.payload - (imr.round, imr) - }.toMap - appRewards <- wallet.store.listSortedAppRewards( - issuingRoundsMap, - PageLimit.tryCreate(maxNumInputs), - ) - inputs: Seq[TransferInput] = (amulets.map(_._2) ++ appRewards.map(a => - new InputAppRewardCoupon(a._1.contractId) - )).take(maxNumInputs) - featuredAppRight <- transferPreapprovalO - .traverse { preapproval => - val provider = PartyId.tryFromProtoPrimitive(preapproval.payload.provider) - scanConnection.lookupFeaturedAppRight(provider) - } - .map(_.flatten) - transferCommandNonce <- context.retryProvider.retry( - RetryFor.Automation, - "wait_for_transfer_command_counter", - s"wait for TransferCommandCounter for $sender", - wallet.store - .lookupTransferCommandCounter() - .map( - _.getOrElse( - throw Status.FAILED_PRECONDITION - .withDescription( - s"No TransferCommandCounter for $sender yet, waiting for SV automation to create it" - ) - .asRuntimeException - ) - ), - logger, - ) - amuletRules <- scanConnection.getAmuletRulesWithState() - rewardInputRounds = appRewards.map(_._1.payload.round).toSet - transferContext = new TransferContext( - openRound.contractId, - openIssuingRounds.view - // only provide rounds that are actually used in transfer context to avoid unnecessary fetching. - .filter(r => rewardInputRounds.contains(r.payload.round)) - .map(r => - ( - r.payload.round, - r.contractId, - ) - ) - .toMap[Round, IssuingMiningRound.ContractId] - .asJava, - // validator right contracts are not visible to DSO - // so cannot use validator rewards. That's fine - // as we also don't expect the external party to have any - Map.empty.asJava, - featuredAppRight.map(_.contractId).toJava, - ) - cmd = transferCommand.exercise( - _.exerciseTransferCommand_Send( - new PaymentTransferContext( - amuletRules.contractId, - transferContext, - ), - inputs.asJava, - transferPreapprovalO.map(_.contractId).toJava, - transferCommandNonce.contractId, - ) - ) - result <- ledgerConnection - .submit( - Seq(store.key.validatorParty), - Seq(sender), - cmd, - ) - .withDisclosedContracts( - ledgerConnection - .disclosedContracts( - amuletRules, - (openRound +: transferPreapprovalO.toList)* - ) - .addAll(openIssuingRounds) - // copy paste the state from amulet rules as we don't currently expose it in scan for featured - // app rights. - .addAll(featuredAppRight.map(ContractWithState(_, amuletRules.state)).toList) - ) - .noDedup - .yieldResult() - } yield TaskSuccess(s"Completed TransferCommand with: $result") - } - } - } -} diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala index fb4b1e42d0b..e32ba715f17 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala @@ -76,7 +76,6 @@ class ValidatorAutomationService( enabledFeatures: EnabledFeaturesConfig, additionalPackagesToUnvet: Map[PackageName, Set[PackageVersion]], globalSynchronizerAlias: SynchronizerAlias, - enableDeprecatedTransferCommandSupport: Boolean, override protected val loggerFactory: NamedLoggerFactory, packageVersionSupport: PackageVersionSupport, )(implicit @@ -196,18 +195,6 @@ class ValidatorAutomationService( domainMigrationId, ) ) - - if (enableDeprecatedTransferCommandSupport) { - registerTrigger( - new TransferCommandSendTrigger( - triggerContext, - scanConnection, - store, - walletManager.externalPartyWalletManager, - connection(SpliceLedgerConnectionPriority.Medium), - ) - ) - } } backupDumpConfig.foreach(config => diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala index 2973fca9803..9dde4c84dab 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala @@ -224,8 +224,6 @@ case class ValidatorAppBackendConfig( // from running concurrently against the same database. Only disable for migration scenarios // where intentional overlap is required. instanceLockEnabled: Boolean = true, - // Enable the deprecated transfer command support, will be fully removed in 0.8.0. - enableDeprecatedTransferCommandSupport: Boolean = false, ) extends SpliceBackendConfig // TODO(DACH-NY/canton-network-node#736): fork or generalize this trait. { override val nodeTypeName: String = "validator" diff --git a/apps/wallet/frontend/src/__tests__/developmentFund.test.tsx b/apps/wallet/frontend/src/__tests__/developmentFund.test.tsx index f81025a0b82..24f2999888e 100644 --- a/apps/wallet/frontend/src/__tests__/developmentFund.test.tsx +++ b/apps/wallet/frontend/src/__tests__/developmentFund.test.tsx @@ -50,6 +50,12 @@ const buildAllocationFormMock = ( setAmount: vi.fn(), expiresAt: dayjs().add(2, 'day'), setExpiresAt: vi.fn(), + mintAfter: dayjs().add(1, 'hour'), + setMintAfter: vi.fn(), + minMintAfter: dayjs(), + isMintAfterRequired: true, + isMintAfterValid: true, + mintAfterError: undefined, reason: 'Valid allocation', setReason: vi.fn(), amountNum: new BigNumber(1), @@ -558,6 +564,7 @@ describe('Development Fund page', () => { test('triggers allocation request on allocate click', async () => { const mutate = vi.fn(); const expiresAt = dayjs().add(2, 'day'); + const mintAfter = dayjs().add(1, 'hour'); const hookSpy = vi .spyOn(developmentFundAllocationFormHook, 'useDevelopmentFundAllocationForm') @@ -570,6 +577,12 @@ describe('Development Fund page', () => { setAmount: vi.fn(), expiresAt, setExpiresAt: vi.fn(), + mintAfter, + setMintAfter: vi.fn(), + minMintAfter: dayjs(), + isMintAfterRequired: true, + isMintAfterValid: true, + mintAfterError: undefined, reason: 'Valid allocation', setReason: vi.fn(), amountNum: new BigNumber(1), @@ -599,8 +612,85 @@ describe('Development Fund page', () => { amount: new BigNumber(1), expiresAt: expiresAt.toDate(), reason: 'Valid allocation', + mintAfter: mintAfter.toDate(), }); hookSpy.mockRestore(); }); + + test('sends the chosen mintAfter with the allocation', async () => { + const mutate = vi.fn(); + const expiresAt = dayjs().add(2, 'day'); + const mintAfter = dayjs().add(1, 'day'); + + const hookSpy = vi + .spyOn(developmentFundAllocationFormHook, 'useDevelopmentFundAllocationForm') + .mockReturnValue( + buildAllocationFormMock({ + expiresAt, + mintAfter, + allocateMutation: { + mutate, + isPending: false, + } as unknown as ReturnType< + typeof developmentFundAllocationFormHook.useDevelopmentFundAllocationForm + >['allocateMutation'], + }) + ); + + const { user } = await loginAndOpenDevelopmentFund(); + await user.click(await screen.findByRole('button', { name: 'Allocate' })); + + expect(mutate).toHaveBeenCalledWith(expect.objectContaining({ mintAfter: mintAfter.toDate() })); + + hookSpy.mockRestore(); + }); + + test('disables Allocate button when mintAfter is invalid', async () => { + const hookSpy = vi + .spyOn(developmentFundAllocationFormHook, 'useDevelopmentFundAllocationForm') + .mockReturnValue( + buildAllocationFormMock({ + mintAfter: null, + isMintAfterValid: false, + mintAfterError: 'Mint after is required', + isValid: false, + }) + ); + + await loginAndOpenDevelopmentFund(); + + expect(await screen.findByRole('button', { name: 'Allocate' })).toBeDisabled(); + expect(screen.getByText('Mint after is required')).toBeDefined(); + + hookSpy.mockRestore(); + }); + + test('marks Mint After optional and stays submittable when no delay is configured', async () => { + const mutate = vi.fn(); + const hookSpy = vi + .spyOn(developmentFundAllocationFormHook, 'useDevelopmentFundAllocationForm') + .mockReturnValue( + buildAllocationFormMock({ + mintAfter: null, + minMintAfter: null, + isMintAfterRequired: false, + allocateMutation: { + mutate, + isPending: false, + } as unknown as ReturnType< + typeof developmentFundAllocationFormHook.useDevelopmentFundAllocationForm + >['allocateMutation'], + }) + ); + + const { user } = await loginAndOpenDevelopmentFund(); + expect(screen.getByLabelText('Mint After (optional)')).toBeDefined(); + + await user.click(await screen.findByRole('button', { name: 'Allocate' })); + + expect(mutate).toHaveBeenCalledWith(expect.objectContaining({ mintAfter: undefined })); + + hookSpy.mockRestore(); + }); }); diff --git a/apps/wallet/frontend/src/__tests__/useDevelopmentFundAllocationForm.test.tsx b/apps/wallet/frontend/src/__tests__/useDevelopmentFundAllocationForm.test.tsx new file mode 100644 index 00000000000..190ef37a8cd --- /dev/null +++ b/apps/wallet/frontend/src/__tests__/useDevelopmentFundAllocationForm.test.tsx @@ -0,0 +1,184 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { act, renderHook } from '@testing-library/react'; +import BigNumber from 'bignumber.js'; +import dayjs, { Dayjs } from 'dayjs'; +import { ReactNode } from 'react'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import useGetAmuletRules from '../hooks/scan-proxy/useGetAmuletRules'; +import { + useDevelopmentFundAllocationForm, + UseDevelopmentFundAllocationFormResult, +} from '../hooks/useDevelopmentFundAllocationForm'; +import { alicePartyId } from './mocks/constants'; + +vi.mock('../hooks/scan-proxy/useGetAmuletRules', () => ({ default: vi.fn() })); + +vi.mock('../hooks/useIsDevelopmentFundManager', () => ({ + useIsDevelopmentFundManager: () => ({ isFundManager: true, isLoading: false }), +})); + +vi.mock('../hooks/useUnclaimedDevelopmentFundTotal', () => ({ + useUnclaimedDevelopmentFundTotal: () => ({ + data: new BigNumber(100), + isLoading: false, + isError: false, + error: null, + invalidate: vi.fn(), + }), +})); + +vi.mock('../contexts/WalletServiceContext', () => ({ + useWalletClient: () => ({ allocateDevelopmentFundCoupon: vi.fn() }), +})); + +const openedAt = new Date('2026-01-15T12:00:00.000Z'); +const sevenDaysInMicros = String(7 * 24 * 60 * 60 * 1_000_000); + +const mockMintingDelay = (minDevelopmentFundMintingDelay: { microseconds: string } | null) => { + vi.mocked(useGetAmuletRules).mockReturnValue({ + data: { + contract: { + payload: { configSchedule: { initialValue: { minDevelopmentFundMintingDelay } } }, + }, + }, + } as unknown as ReturnType); +}; + +const renderForm = () => { + const queryClient = new QueryClient(); + return renderHook(() => useDevelopmentFundAllocationForm(), { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); +}; + +const fillRequiredFields = ( + result: { current: UseDevelopmentFundAllocationFormResult }, + expiresAt: Dayjs +) => + act(() => { + result.current.setBeneficiary(alicePartyId); + result.current.setAmount('1'); + result.current.setExpiresAt(expiresAt); + result.current.setReason('Valid allocation'); + }); + +describe('useDevelopmentFundAllocationForm', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(openedAt); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + // Happy + test('mintAfter is unset by default and optional when no minDevelopmentFundMintingDelay is configured', () => { + mockMintingDelay(null); + + const { result } = renderForm(); + fillRequiredFields(result, dayjs(openedAt).add(2, 'day')); + + expect(result.current.isMintAfterRequired).toBe(false); + expect(result.current.minMintAfter).toBeNull(); + expect(result.current.mintAfter).toBeNull(); + expect(result.current.mintAfterError).toBeUndefined(); + expect(result.current.isValid).toBe(true); + }); + + // Happy + test('mintAfter is acceptable even when no minDevelopmentFundMintingDelay is configured', () => { + mockMintingDelay(null); + + const { result } = renderForm(); + fillRequiredFields(result, dayjs(openedAt).add(2, 'day')); + act(() => result.current.setMintAfter(dayjs(openedAt).add(1, 'day'))); + + expect(result.current.isMintAfterRequired).toBe(false); + expect(result.current.minMintAfter).toBeNull(); + expect(result.current.mintAfter?.toISOString()).toBe('2026-01-16T12:00:00.000Z'); + expect(result.current.mintAfterError).toBeUndefined(); + expect(result.current.isValid).toBe(true); + }); + + // Happy + test('mintAfter is required and pre-filled when the configured minting delay is zero', () => { + mockMintingDelay({ microseconds: '0' }); + + const { result } = renderForm(); + fillRequiredFields(result, dayjs(openedAt).add(2, 'day')); + + expect(result.current.isMintAfterRequired).toBe(true); + expect(result.current.minMintAfter?.toISOString()).toBe('2026-01-15T12:00:00.000Z'); + expect(result.current.mintAfter?.toISOString()).toBe('2026-01-15T13:00:00.000Z'); + expect(result.current.mintAfterError).toBeUndefined(); + expect(result.current.isValid).toBe(true); + }); + + // Happy + test('offsets minMintAfter by the configured minting delay', () => { + mockMintingDelay({ microseconds: sevenDaysInMicros }); + + const { result } = renderForm(); + fillRequiredFields(result, dayjs(openedAt).add(30, 'day')); + + expect(result.current.isMintAfterRequired).toBe(true); + expect(result.current.minMintAfter?.toISOString()).toBe('2026-01-22T12:00:00.000Z'); + expect(result.current.mintAfter?.toISOString()).toBe('2026-01-22T13:00:00.000Z'); + expect(result.current.mintAfterError).toBeUndefined(); + expect(result.current.isValid).toBe(true); + }); + + // Unhappy + test('requires a mintAfter after the fund manager clears the field', () => { + mockMintingDelay({ microseconds: sevenDaysInMicros }); + + const { result } = renderForm(); + fillRequiredFields(result, dayjs(openedAt).add(2, 'day')); + act(() => result.current.setMintAfter(null)); + + expect(result.current.mintAfterError).toBe('Mint after is required'); + expect(result.current.isMintAfterValid).toBe(false); + expect(result.current.isValid).toBe(false); + }); + + // Unhappy + test('rejects a mintAfter that fell into the past while the form stayed open', () => { + mockMintingDelay(null); + + const { result } = renderForm(); + vi.setSystemTime(dayjs(openedAt).add(2, 'hour').toDate()); + act(() => result.current.setMintAfter(dayjs(openedAt).add(1, 'hour'))); + + expect(result.current.mintAfterError).toBe('Mint after must be in the future'); + expect(result.current.isMintAfterValid).toBe(false); + }); + + // Unhappy + test('rejects a mintAfter earlier than the configured minting delay allows', () => { + mockMintingDelay({ microseconds: sevenDaysInMicros }); + + const { result } = renderForm(); + act(() => result.current.setMintAfter(dayjs(openedAt).add(1, 'day'))); + + expect(result.current.mintAfterError).toMatch(/^Mint after must be at or after /); + expect(result.current.isMintAfterValid).toBe(false); + }); + + // Unhappy + test('rejects a mintAfter that is not before the expiry', () => { + mockMintingDelay(null); + + const { result } = renderForm(); + fillRequiredFields(result, dayjs(openedAt).add(2, 'day')); + act(() => result.current.setMintAfter(dayjs(openedAt).add(3, 'day'))); + + expect(result.current.mintAfterError).toBe('Mint after must be before the expiry'); + expect(result.current.isValid).toBe(false); + }); +}); diff --git a/apps/wallet/frontend/src/components/DevelopmentFundAllocation.tsx b/apps/wallet/frontend/src/components/DevelopmentFundAllocation.tsx index a86f9f3b570..ad86444269a 100644 --- a/apps/wallet/frontend/src/components/DevelopmentFundAllocation.tsx +++ b/apps/wallet/frontend/src/components/DevelopmentFundAllocation.tsx @@ -30,6 +30,11 @@ const DevelopmentFundAllocation: React.FC = () => { setAmount, expiresAt, setExpiresAt, + mintAfter, + setMintAfter, + minMintAfter, + isMintAfterRequired, + mintAfterError, reason, setReason, amountNum, @@ -124,6 +129,26 @@ const DevelopmentFundAllocation: React.FC = () => { }} /> + + + Mint After + setMintAfter(newValue)} + minDateTime={minMintAfter ?? undefined} + disabled={disabled} + enableAccessibleFieldDOMStructure={false} + slotProps={{ + textField: { + id: 'development-fund-allocation-mint-after', + fullWidth: true, + error: !!mintAfterError, + helperText: mintAfterError, + }, + }} + /> + Reason @@ -167,6 +192,7 @@ const DevelopmentFundAllocation: React.FC = () => { amount: amountNum, expiresAt: expiresAt.toDate(), reason, + mintAfter: mintAfter?.toDate(), }) } > diff --git a/apps/wallet/frontend/src/components/DevelopmentFundCouponList.tsx b/apps/wallet/frontend/src/components/DevelopmentFundCouponList.tsx index 21955345904..5e6287c85f1 100644 --- a/apps/wallet/frontend/src/components/DevelopmentFundCouponList.tsx +++ b/apps/wallet/frontend/src/components/DevelopmentFundCouponList.tsx @@ -125,6 +125,7 @@ const ActiveCouponsTable: React.FC = () => { Beneficiary Amount Expires At + Mint After Allocation Reason Actions @@ -132,7 +133,7 @@ const ActiveCouponsTable: React.FC = () => { {coupons.length === 0 ? ( - + No development fund allocations found @@ -153,6 +154,15 @@ const ActiveCouponsTable: React.FC = () => { + + {coupon.mintAfter ? ( + + ) : ( + + - + + )} + {coupon.reason} {coupon.fundManager === primaryParty ? ( diff --git a/apps/wallet/frontend/src/contexts/WalletServiceContext.tsx b/apps/wallet/frontend/src/contexts/WalletServiceContext.tsx index 6dde43d82a6..270a0a7d39e 100644 --- a/apps/wallet/frontend/src/contexts/WalletServiceContext.tsx +++ b/apps/wallet/frontend/src/contexts/WalletServiceContext.tsx @@ -167,7 +167,8 @@ export interface WalletClient { beneficiary: string, amount: BigNumber, expiresAt: Date, - reason: string + reason: string, + mintAfter?: Date ) => Promise; listActiveDevelopmentFundCoupons: () => Promise; listCouponHistoryEvents: ( @@ -509,13 +510,15 @@ export const WalletClientProvider: React.FC beneficiary: string, amount: BigNumber, expiresAt: Date, - reason: string + reason: string, + mintAfter?: Date ): Promise => { const request = { beneficiary: beneficiary, amount: amount.isInteger() ? amount.toFixed(1) : amount.toString(), expiresAt: expiresAt.getTime() * 1000, reason: reason, + mintAfter: mintAfter ? mintAfter.getTime() * 1000 : undefined, }; await walletClient.allocateDevelopmentFundCoupon(request); }, @@ -530,6 +533,9 @@ export const WalletClientProvider: React.FC beneficiary: contract.payload.beneficiary, amount: new BigNumber(contract.payload.amount), expiresAt: new Date(contract.payload.expiresAt), + mintAfter: contract.payload.mintAfter + ? new Date(contract.payload.mintAfter) + : undefined, reason: contract.payload.reason, }; }); diff --git a/apps/wallet/frontend/src/hooks/useDevelopmentFundAllocationForm.ts b/apps/wallet/frontend/src/hooks/useDevelopmentFundAllocationForm.ts index d3dfe401c2c..7a41043f840 100644 --- a/apps/wallet/frontend/src/hooks/useDevelopmentFundAllocationForm.ts +++ b/apps/wallet/frontend/src/hooks/useDevelopmentFundAllocationForm.ts @@ -4,6 +4,7 @@ import { useState, useMemo } from 'react'; import { useMutation, useQueryClient, UseMutationResult } from '@tanstack/react-query'; import { extractApiErrorMessage } from '@canton-network/splice-common-frontend'; import { useWalletClient } from '../contexts/WalletServiceContext'; +import useGetAmuletRules from './scan-proxy/useGetAmuletRules'; import { useIsDevelopmentFundManager } from './useIsDevelopmentFundManager'; import { useUnclaimedDevelopmentFundTotal } from './useUnclaimedDevelopmentFundTotal'; import { invalidateAllDevelopmentFundQueries } from '../utils/invalidateDevelopmentFundQueries'; @@ -19,6 +20,12 @@ export interface UseDevelopmentFundAllocationFormResult { setAmount: (value: string) => void; expiresAt: Dayjs | null; setExpiresAt: (value: Dayjs | null) => void; + mintAfter: Dayjs | null; + setMintAfter: (value: Dayjs | null) => void; + minMintAfter: Dayjs | null; + isMintAfterRequired: boolean; + isMintAfterValid: boolean; + mintAfterError: string | undefined; reason: string; setReason: (value: string) => void; amountNum: BigNumber | null; @@ -39,12 +46,16 @@ interface AllocationPayload { amount: BigNumber; expiresAt: Date; reason: string; + mintAfter?: Date; } +const SUBMISSION_HEADROOM_HOURS = 1; + export const useDevelopmentFundAllocationForm = (): UseDevelopmentFundAllocationFormResult => { const { allocateDevelopmentFundCoupon } = useWalletClient(); const { isFundManager } = useIsDevelopmentFundManager(); const { data: unclaimedTotal } = useUnclaimedDevelopmentFundTotal(); + const { data: amuletRulesData } = useGetAmuletRules(); const queryClient = useQueryClient(); const [formKey, setFormKey] = useState(0); @@ -52,8 +63,28 @@ export const useDevelopmentFundAllocationForm = (): UseDevelopmentFundAllocation const [beneficiary, setBeneficiary] = useState(''); const [amount, setAmount] = useState(''); const [expiresAt, setExpiresAt] = useState(null); + const [mintAfterOverride, setMintAfterOverride] = useState(undefined); + const [defaultBaseTime, setDefaultBaseTime] = useState(() => dayjs()); const [reason, setReason] = useState(''); + const minMintingDelayMicros = + amuletRulesData?.contract.payload.configSchedule.initialValue.minDevelopmentFundMintingDelay + ?.microseconds; + const isMintAfterRequired = minMintingDelayMicros !== undefined; + const { minMintAfter, defaultMintAfter } = useMemo(() => { + if (minMintingDelayMicros === undefined) { + return { minMintAfter: null, defaultMintAfter: null }; + } + const earliest = defaultBaseTime.add(Number(minMintingDelayMicros) / 1000, 'millisecond'); + return { + minMintAfter: earliest, + defaultMintAfter: earliest.add(SUBMISSION_HEADROOM_HOURS, 'hour'), + }; + }, [minMintingDelayMicros, defaultBaseTime]); + + const mintAfter = mintAfterOverride !== undefined ? mintAfterOverride : defaultMintAfter; + const setMintAfter = (value: Dayjs | null) => setMintAfterOverride(value); + const amountNum = useMemo(() => (amount ? new BigNumber(amount) : null), [amount]); const isAmountValid = amountNum !== null && amountNum.isFinite() && amountNum.gt(0); const amountExceedsAvailable = isAmountValid && amountNum.gt(unclaimedTotal); @@ -67,11 +98,33 @@ export const useDevelopmentFundAllocationForm = (): UseDevelopmentFundAllocation : undefined : undefined; const isReasonValid = reason.trim().length > 0; + + const mintAfterError = (() => { + if (mintAfter == null) { + return isMintAfterRequired ? 'Mint after is required' : undefined; + } + if (!mintAfter.isValid()) { + return 'Invalid date'; + } + if (minMintAfter != null && mintAfter.isBefore(minMintAfter)) { + return `Mint after must be at or after ${minMintAfter.format('MMM D, YYYY hh:mm A')}`; + } + if (!mintAfter.isAfter(dayjs())) { + return 'Mint after must be in the future'; + } + if (expiresAt != null && expiresAt.isValid() && !mintAfter.isBefore(expiresAt)) { + return 'Mint after must be before the expiry'; + } + return undefined; + })(); + const isMintAfterValid = mintAfterError === undefined; + const isValid = Boolean(beneficiary) && isAmountValid && !amountExceedsAvailable && isExpiryValid && + isMintAfterValid && isReasonValid; const resetForm = () => { @@ -79,6 +132,8 @@ export const useDevelopmentFundAllocationForm = (): UseDevelopmentFundAllocation setBeneficiary(''); setAmount(''); setExpiresAt(null); + setMintAfterOverride(undefined); + setDefaultBaseTime(dayjs()); setReason(''); setFormKey(prev => prev + 1); }; @@ -89,7 +144,8 @@ export const useDevelopmentFundAllocationForm = (): UseDevelopmentFundAllocation data.beneficiary, data.amount, data.expiresAt, - data.reason + data.reason, + data.mintAfter ); }, onSuccess: () => { @@ -111,6 +167,12 @@ export const useDevelopmentFundAllocationForm = (): UseDevelopmentFundAllocation setAmount, expiresAt, setExpiresAt, + mintAfter, + setMintAfter, + minMintAfter, + isMintAfterRequired, + isMintAfterValid, + mintAfterError, reason, setReason, amountNum, diff --git a/apps/wallet/frontend/src/models/models.ts b/apps/wallet/frontend/src/models/models.ts index b7af05928e5..25c6391bf7f 100644 --- a/apps/wallet/frontend/src/models/models.ts +++ b/apps/wallet/frontend/src/models/models.ts @@ -138,6 +138,7 @@ export interface DevelopmentFundCoupon { beneficiary: string; amount: BigNumber; expiresAt: Date; + mintAfter?: Date; reason: string; withdrawalReason?: string; } diff --git a/apps/wallet/src/main/openapi/wallet-internal.yaml b/apps/wallet/src/main/openapi/wallet-internal.yaml index cad63441055..65cc5aa0cff 100644 --- a/apps/wallet/src/main/openapi/wallet-internal.yaml +++ b/apps/wallet/src/main/openapi/wallet-internal.yaml @@ -2220,6 +2220,11 @@ components: format: int64 reason: type: string + mintAfter: + type: integer + format: int64 + description: | + Earliest time at which the beneficiary may mint the coupon, in epoch microseconds. AllocateDevelopmentFundCouponResponse: type: object diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/api/client/commands/HttpWalletAppClient.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/api/client/commands/HttpWalletAppClient.scala index 2536451b666..e9841e389a4 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/api/client/commands/HttpWalletAppClient.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/api/client/commands/HttpWalletAppClient.scala @@ -1616,6 +1616,7 @@ object HttpWalletAppClient { amount: BigDecimal, expiresAt: CantonTimestamp, reason: String, + mintAfter: Option[CantonTimestamp] = None, ) extends InternalBaseCommand[ http.AllocateDevelopmentFundCouponResponse, definitions.AllocateDevelopmentFundCouponResponse, @@ -1632,6 +1633,7 @@ object HttpWalletAppClient { Codec.encode(amount), Codec.encode(expiresAt), reason, + mintAfter.map(Codec.encode(_)), ), headers = headers, ) diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala index bb5b781c4df..dd2d4b92f8f 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/admin/http/HttpWalletHandler.scala @@ -1773,6 +1773,7 @@ class HttpWalletHandler( beneficiary = Codec.tryDecode(Codec.Party)(body.beneficiary) amount = Codec.tryDecode(Codec.BigDecimal)(body.amount) expiresAt = Codec.tryDecode(Codec.Timestamp)(body.expiresAt) + mintAfter = body.mintAfter.map(Codec.tryDecode(Codec.Timestamp)(_).toInstant) optDevelopmentFundManager = amuletRulesCt.contract.payload.configSchedule.initialValue.optDevelopmentFundManager .map(Codec.tryDecode(Codec.Party)(_)) @@ -1814,6 +1815,7 @@ class HttpWalletHandler( expiresAt.toInstant, body.reason, developmentFundManager.toProtoPrimitive, + mintAfter.toJava, ) ) result <- userWallet.connection @@ -1828,7 +1830,9 @@ class HttpWalletHandler( .CommandId( "org.lfdecentralizedtrust.splice.wallet.allocateDevelopmentFundCoupon", Seq(store.key.validatorParty, store.key.endUserParty), - Seq(s"${body.beneficiary}:${body.amount}:${body.expiresAt}:${body.reason}"), + Seq( + s"${body.beneficiary}:${body.amount}:${body.expiresAt}:${body.reason}:${body.mintAfter}" + ), ), deduplicationConfig = dedupDuration, ) diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala index 925ef6ad081..b3fee8f9e50 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/automation/MintingDelegationCollectRewardsTrigger.scala @@ -53,6 +53,7 @@ import org.lfdecentralizedtrust.splice.util.{ } import org.lfdecentralizedtrust.splice.wallet.config.RewardSharingConfig import org.lfdecentralizedtrust.splice.wallet.store.ExternalPartyWalletStore +import org.lfdecentralizedtrust.splice.wallet.util.DevelopmentFundCouponUtil import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting} import com.digitalasset.canton.util.ShowUtil.* import org.lfdecentralizedtrust.splice.util.PrettyInstances.* @@ -361,13 +362,16 @@ class MintingDelegationCollectRewardsTrigger( limit = HardLimit.tryCreate(rewardSharingConfig.batchSize), ) unclaimedActivityRecords <- store.listUnclaimedActivityRecords() - developmentFundCoupons <- store.listDevelopmentFundCoupons() + allDevelopmentFundCoupons <- store.listDevelopmentFundCoupons() + mintableDevelopmentFundCoupons = allDevelopmentFundCoupons.filter( + DevelopmentFundCouponUtil.isMintable(_, context.clock.now.toInstant) + ) } yield CouponsData( livenessActivityRecordsWithQuantity.map(_._1), validatorRewardCoupons, appRewardCouponsWithQuantity.map(_._1), unclaimedActivityRecords, - developmentFundCoupons, + mintableDevelopmentFundCoupons, rewardCouponsV2.map(_.contract), ) } diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala index 2ab576f0709..8ed5c21b00e 100644 --- a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/treasury/TreasuryService.scala @@ -64,6 +64,7 @@ import org.lfdecentralizedtrust.splice.wallet.UserWalletManager import org.lfdecentralizedtrust.splice.wallet.config.TreasuryConfig import org.lfdecentralizedtrust.splice.wallet.metrics.TreasuryMetrics import org.lfdecentralizedtrust.splice.wallet.store.UserWalletStore +import org.lfdecentralizedtrust.splice.wallet.util.DevelopmentFundCouponUtil import org.lfdecentralizedtrust.splice.wallet.treasury.TreasuryService.* import com.digitalasset.base.error.utils.ErrorDetails import com.digitalasset.base.error.utils.ErrorDetails.ErrorInfoDetail @@ -1296,9 +1297,10 @@ class TreasuryService( tc: TraceContext ): Future[(BigDecimal, Seq[(BigDecimal, InputDevelopmentFundCoupon)])] = for { - developmentFundCouponsInputs <- userStore.listDevelopmentFundCoupons( - PageLimit.tryCreate(maxNumInputs) - ) + allDevelopmentFundCoupons <- userStore.listDevelopmentFundCoupons() + developmentFundCouponsInputs = allDevelopmentFundCoupons + .filter(DevelopmentFundCouponUtil.isMintable(_, clock.now.toInstant)) + .take(maxNumInputs) developmentFundCouponsQuantity = developmentFundCouponsInputs .map(coupon => scala.math.BigDecimal(coupon.payload.amount)) .sum diff --git a/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/util/DevelopmentFundCouponUtil.scala b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/util/DevelopmentFundCouponUtil.scala new file mode 100644 index 00000000000..6482e5736f1 --- /dev/null +++ b/apps/wallet/src/main/scala/org/lfdecentralizedtrust/splice/wallet/util/DevelopmentFundCouponUtil.scala @@ -0,0 +1,19 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package org.lfdecentralizedtrust.splice.wallet.util + +import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.DevelopmentFundCoupon +import org.lfdecentralizedtrust.splice.util.Contract + +import java.time.Instant +import scala.jdk.OptionConverters.* + +object DevelopmentFundCouponUtil { + + def isMintable( + coupon: Contract[DevelopmentFundCoupon.ContractId, DevelopmentFundCoupon], + now: Instant, + ): Boolean = + coupon.payload.mintAfter.toScala.forall(mintAfter => !mintAfter.isAfter(now)) +} diff --git a/apps/wallet/src/test/scala/org/lfdecentralizedtrust/splice/store/db/UserWalletStoreTest.scala b/apps/wallet/src/test/scala/org/lfdecentralizedtrust/splice/store/db/UserWalletStoreTest.scala index da3e0a1bfd0..3b5308662ed 100644 --- a/apps/wallet/src/test/scala/org/lfdecentralizedtrust/splice/store/db/UserWalletStoreTest.scala +++ b/apps/wallet/src/test/scala/org/lfdecentralizedtrust/splice/store/db/UserWalletStoreTest.scala @@ -1895,6 +1895,7 @@ abstract class UserWalletStoreTest extends TransferInputStoreTest with HasExecut expiresAt.toInstant, reason, fundManager.toProtoPrimitive, + Optional.empty(), // mintAfter ).toValue, new amuletRulesCodegen.AmuletRules_AllocateDevelopmentFundCouponResult( contractId, @@ -1931,6 +1932,7 @@ abstract class UserWalletStoreTest extends TransferInputStoreTest with HasExecut damlDecimal(amount), expiresAt.toInstant, reason, + Optional.empty(), // mintAfter ), ) } diff --git a/build-tools/splice-compose.sh b/build-tools/splice-compose.sh index 3da073cd279..164ee3b5ac6 100755 --- a/build-tools/splice-compose.sh +++ b/build-tools/splice-compose.sh @@ -18,6 +18,27 @@ SV_DIR="${SPLICE_ROOT}/cluster/compose/sv" # we want DEBUG logs for our tests export LOG_LEVEL=DEBUG +mkdir -p "${SPLICE_ROOT}/log" +exec > >(tee -a "${SPLICE_ROOT}/log/compose.log") 2>&1 + +function _dump_postgres_logs { + local c + for c in splice-validator-postgres-splice-1 splice-sv-postgres-splice-sv-1; do + if docker container inspect "$c" > /dev/null 2>&1; then + _info "Dumping postgres logs for $c" + docker logs "$c" > "${SPLICE_ROOT}/log/compose-postgres-${c}.clog" 2>&1 || true + fi + done +} + +# Dump postgres logs whenever the script exits with a non-zero code +function _on_error_exit { + if [ "$1" -ne 0 ]; then + _dump_postgres_logs + fi +} +trap '_on_error_exit $?' EXIT + function _export_auth0_env_vars { if [ -z "$GCP_CLUSTER_BASENAME" ]; then @@ -58,7 +79,7 @@ function _export_auth0_env_vars { function _do_start_validator { "${VALIDATOR_DIR}/start.sh" \ "$@" \ - | tee -a "${SPLICE_ROOT}/log/compose.log" 2>&1 || _error "Failed to start validator, please check ${SPLICE_ROOT}/log/compose.log for details" + || _error "Failed to start validator, please check ${SPLICE_ROOT}/log/compose.log for details" for c in validator participant nginx; do docker logs -f splice-validator-${c}-1 >> "${SPLICE_ROOT}/log/compose-${c}.clog" 2>&1 & @@ -70,7 +91,7 @@ function _do_start_validator { "${VALIDATOR_DIR}/start.sh" \ "$@" \ "-w" \ - | tee -a "${SPLICE_ROOT}/log/compose-wait.log" 2>&1 || _error "Validator failed to become ready" + || _error "Validator failed to become ready" fi } diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala index 3b512df9f86..0dde844a368 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ParticipantNode.scala @@ -597,21 +597,24 @@ class ParticipantNodeBootstrap( loggerFactory, ) - trafficEnforcementBackendContainerO = Option.when(config.trafficEnforcement.enabled)( - new LifeCycleContainer( - stateName = "traffic-enforcement-backend", - create = () => - FutureUnlessShutdown.pure( - TrafficEnforcementBackend( - trafficEnforcementServerConfig = - config.trafficEnforcement.trafficEnforcementServer, - processingTimeout = timeouts, - loggerFactory = loggerFactory, - ) - ), - loggerFactory = loggerFactory, - ) - ) + trafficEnforcementBackendContainerO: Option[LifeCycleContainer[TrafficEnforcementBackend]] = + None + // stubbed in splice + // Option.when(config.trafficEnforcement.enabled)( + // new LifeCycleContainer( + // stateName = "traffic-enforcement-backend", + // create = () => + // FutureUnlessShutdown.pure( + // TrafficEnforcementBackend( + // trafficEnforcementServerConfig = + // config.trafficEnforcement.trafficEnforcementServer, + // processingTimeout = timeouts, + // loggerFactory = loggerFactory, + // ) + // ), + // loggerFactory = loggerFactory, + // ) + // ) _ <- trafficEnforcementBackendContainerO.traverseTap { trafficEnforcementBackendContainer => // only initialize traffic enforcement backend if participant is becoming active diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala index 20558ed5ad9..9bead8e36ae 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/ledger/api/LedgerApiServer.scala @@ -424,7 +424,7 @@ class LedgerApiServer( apiLoggingConfig = cantonParameterConfig.loggingConfig.api, apiContractService = apiContractService, safeToPruneCommitmentState = pruningConfig.safeToPruneCommitmentState, - trafficEnforcementBackendO = trafficEnforcementBackendO.map(_.value), + trafficEnforcementBackendO = trafficEnforcementBackendO, ) _ <- startHttpApiIfEnabled( timedSyncService, diff --git a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala index ced5191dd03..656bf8bab10 100644 --- a/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala +++ b/canton/community/participant/src/main/scala/com/digitalasset/canton/participant/protocol/TransactionProcessor.scala @@ -4,7 +4,6 @@ package com.digitalasset.canton.participant.protocol import cats.data.EitherT -import cats.implicits.toTraverseOps import com.daml.metrics.api.MetricsContext import com.digitalasset.base.error.{ Alarm, @@ -20,7 +19,6 @@ import com.digitalasset.canton.config.{ProcessingTimeout, TestingConfigInternal} import com.digitalasset.canton.crypto.* import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.data.ViewType.TransactionViewType -import com.digitalasset.canton.discard.Implicits.DiscardOps import com.digitalasset.canton.error.* import com.digitalasset.canton.error.CantonErrorGroups.ParticipantErrorGroup.TransactionErrorGroup.SubmissionErrorGroup import com.digitalasset.canton.ledger.error.groups.ConsistencyErrors @@ -155,15 +153,16 @@ class TransactionProcessor( )( trafficCost: Long, traceContext: TraceContext, - ): FutureUnlessShutdown[Unit] = - trafficEnforcementBackendO - .traverse( - _.validateTraffic( - actAs = submissionParam.submitterInfo.actAs, - trafficCost = trafficCost, - )(traceContext) - ) - .map(_.discard) + ): FutureUnlessShutdown[Unit] = ??? + // stubbed in splice + // trafficEnforcementBackendO + // .traverse( + // _.validateTraffic( + // actAs = submissionParam.submitterInfo.actAs, + // trafficCost = trafficCost, + // )(traceContext) + // ) + // .map(_.discard) def submit( submitterInfo: SubmitterInfo, diff --git a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala index 4bccb521b3b..7ca1cdb13d1 100644 --- a/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala +++ b/canton/community/traffic-enforcement/component/src/main/scala/com/digitalasset/canton/tea/projection/db/TeaDbProjection.scala @@ -18,7 +18,7 @@ import com.typesafe.config.{Config, ConfigFactory} import org.apache.pekko.Done import org.apache.pekko.actor.typed.{ActorSystem, Behavior} import org.apache.pekko.projection.slick.{SlickHandler, SlickProjection} -import org.apache.pekko.projection.{HandlerRecoveryStrategy, ProjectionBehavior, ProjectionId} +import org.apache.pekko.projection.{ProjectionBehavior, ProjectionId} import org.apache.pekko.stream.scaladsl.Source import slick.basic.DatabaseConfig import slick.dbio.DBIO @@ -74,10 +74,7 @@ private[projection] class TeaDbProjection( handler = () => eventHandler(projectionId), ) .withStatusObserver(loggingObserver) - .withRecoveryStrategy( - HandlerRecoveryStrategy - .retryAndFail(config.maxRetries.value, config.retryDelay.asFiniteApproximation) - ) + // Removed retry strategy in splice as it doesn't compile against the new canton library version and this code isn't actually used. } // Event handler for projection events diff --git a/canton/community/util-observability/src/main/resources/rewrite-appender.xml b/canton/community/util-observability/src/main/resources/rewrite-appender.xml deleted file mode 100644 index 37165248813..00000000000 --- a/canton/community/util-observability/src/main/resources/rewrite-appender.xml +++ /dev/null @@ -1,351 +0,0 @@ - - - - - - ${REWRITE_TESTING:-false} - - - - - - - - io.grpc.netty.shaded.io.grpc.netty.NettyServerStream - INFO - Stream closed before write could take place - - - - io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - INFO - Stream Error - Received DATA frame for an unknown stream - - - io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - INFO - Stream Error - Stream closed before write could take place - - - - com.zaxxer.hikari.pool.ProxyConnection - INFO - marked as broken - - - - org.apache.pekko.stream.Materializer - INFO - [completion at client] Upstream failed - UNAVAILABLE - - - - - - com.github.benmanes.caffeine.cache.LocalAsyncCache - - Exception thrown during asynchronous load - INFO - - - - - com.digitalasset.canton.platform.indexer.RecoveringIndexer - Error while running indexer, restart scheduled - MERGE INTO packages USING DUAL on package_id - INFO - - - com.digitalasset.canton.platform.store.appendonlydao.DbDispatcher - INDEX_DB_SQL_NON_TRANSIENT_ERROR - MERGE INTO packages USING DUAL on package_id - INFO - - - - - org.flywaydb.core.internal.database.base.Database - Flyway upgrade recommended: H2 2.1.210 is newer than this version of Flyway and support has not been tested. - INFO - - - - - com.zaxxer.hikari.pool.PoolBase - Failed to validate connection - INFO - - - - - io.grpc.netty.shaded.io.grpc.netty.NettyServerStream - INFO - Exception processing message - gRPC message exceeds maximum size - true - - - - org.flywaydb.core.Flyway - INFO - cleanOnValidationError is deprecated and will be removed in a later release - true - - - - com.digitalasset.canton.auth.AuthInterceptor - INFO - The command is missing a (valid) JWT token - true - - - - - software.amazon.awssdk.http.nio.grpc.netty.shaded.io.netty.internal.SslContextProvider - INFO - SSL Certificate verification is disabled - true - - - - - - io.grpc.internal.ManagedChannelImpl - - Failed to resolve name. - INFO - true - - - - io.grpc.internal.SerializingExecutor - test exception - INFO - true - - - com.daml.jwt.HMAC256Verifier$ - HMAC256 JWT Validator is NOT recommended for production environments - INFO - true - - - com.digitalasset.canton.platform.store.FlywayMigrations - Asked to migrate-on-empty-schema - INFO - true - - - - com.digitalasset.canton.platform.store.appendonlydao.DbDispatcher - Exception while executing SQL query. Rolled back. - pruned offset - INFO - true - - - - com.digitalasset.canton.platform.store.appendonlydao.DbDispatcher - Processing the request failed due to a non-transient database error: Unique index or primary key violation - Unique index or primary key violation - INFO - true - - - - com.digitalasset.canton.integration.tests.logging.LoggingIntegrationTestDefault - INFO - true - - - - - com.digitalasset.canton.auth.Authorizer - INFO - true - - - - - com.digitalasset.canton.ledger.api.auth.UserBasedOngoingAuthorization - INFO - true - - - - - daml.warnings - Use of divulged contracts is deprecated and incompatible with pruning. - INFO - true - - - - - org.testcontainers.shaded.okhttp3.OkHttpClient - INFO - A connection to http://docker.socket/ was leaked. Did you forget to close a response body? - true - - - - - 🐳 [postgres:11] - INFO - Retrying pull for image: postgres - true - - - 🐳 [postgres:11] - INFO - Docker image pull has not made progress in - true - - - - - com.digitalasset.canton.config.CommunityConfigValidations$ - INFO - DEPRECATED_PROTOCOL_VERSION - true - - - - - io.grpc.protobuf.services.HealthCheckingLoadBalancerFactory - UNIMPLEMENTED - INFO - - - - - com.digitalasset.canton.platform.apiserver.error.ErrorInterceptor - LEDGER_API_INTERNAL_ERROR - INTERNAL: Task com.codahale.metrics - INFO - true - - - - - com.digitalasset.canton.platform.apiserver.SeedService$ - contract ID - INFO - true - - - - - com.digitalasset.canton.concurrent.ExecutionContextMonitor - - INFO - true - - - - com.digitalasset.canton.participant.sync.ConnectedSynchronizer - Late processing (or clock skew) of batch with counter - INFO - true - - - com.digitalasset.canton.synchronizer.mediator.Mediator - Late processing (or clock skew) of batch with counter - INFO - true - - - - - com.digitalasset.canton.time.TimeProofRequestSubmitterImpl - java.util.concurrent.RejectedExecutionException: null - INFO - true - - - - com.digitalasset.canton.participant.admin.PingService:PocDocumentationIntegrationTest - Failed ping - INFO - true - - - - com.zaxxer.hikari.pool.HikariPool - Timed-out waiting for add connection executor to shutdown - INFO - true - - - - com.digitalasset.canton.participant.pruning.AcsCommitmentProcessor:DemoExampleReferenceIntegrationTest - ACS_COMMITMENT_DEGRADATION - INFO - true - - - - com.digitalasset.canton.synchronizer.sequencer.time.TimeAdvancingTopologySubscriberV1 - Could not send a time-advancing message - INFO - true - - - - - io.opentelemetry.sdk.metrics.internal.state.AsynchronousMetricStorage - INFO - true - - - - diff --git a/cluster/compose/localnet/compose.env b/cluster/compose/localnet/compose.env index f3653ca0951..d8d63b90530 100644 --- a/cluster/compose/localnet/compose.env +++ b/cluster/compose/localnet/compose.env @@ -4,7 +4,7 @@ IMAGE_REPO=${IMAGE_REPO:-ghcr.io/digital-asset/decentralized-canton-sync/docker/} # Publicly released images -POSTGRES_VERSION=${POSTGRES_VERSION:-14} +POSTGRES_VERSION=${POSTGRES_VERSION:-14.24-trixie} NGINX_VERSION=${NGINX_VERSION:-1.27.0} DOCKER_NETWORK=${DOCKER_NETWORK:-localnet} diff --git a/cluster/compose/sv/.env b/cluster/compose/sv/.env index 2e62bfe0dcd..78bbc434104 100644 --- a/cluster/compose/sv/.env +++ b/cluster/compose/sv/.env @@ -1,7 +1,7 @@ IMAGE_REPO=ghcr.io/digital-asset/decentralized-canton-sync/docker/ # Versions -SPLICE_POSTGRES_VERSION=14 +SPLICE_POSTGRES_VERSION=14.24-trixie NGINX_VERSION=1.27.1 # Database diff --git a/cluster/compose/validator/.env b/cluster/compose/validator/.env index 370bb1337b9..8a1d6ca9810 100644 --- a/cluster/compose/validator/.env +++ b/cluster/compose/validator/.env @@ -1,7 +1,7 @@ IMAGE_REPO=ghcr.io/digital-asset/decentralized-canton-sync/docker/ # Versions -SPLICE_POSTGRES_VERSION=14 +SPLICE_POSTGRES_VERSION=14.24-trixie NGINX_VERSION=1.27.1 # Database diff --git a/cluster/configs/shared/base.yaml b/cluster/configs/shared/base.yaml index aaf27664396..80b58c11bd0 100644 --- a/cluster/configs/shared/base.yaml +++ b/cluster/configs/shared/base.yaml @@ -25,11 +25,17 @@ infra: istio: enableIngressAccessLogging: true enablePublicTokenRegistry: true - sequencerFlowControl: + flowControl: # See https://github.com/DACH-NY/canton-network-internal/issues/4901 # The values are somewhat arbitrary so more fine tuning may be appropriate. - initialStreamWindowSize: 524288 # 512 KiB max per stream - initialConnectionWindowSize: 52428800 # 50 MiB max per connection + public: + initialStreamWindowSize: 524288 # 512 KiB max per stream + initialConnectionWindowSize: 52428800 # 50 MiB max per connection + ports: [5008, 5010] # sequencer public api, canton bft + internal: + initialStreamWindowSize: 1048576 # 1 MiB max per stream + initialConnectionWindowSize: 104857600 # 100 MiB max per connection + ports: [5001, 5002, 5007, 5009] # ledger API, participant admin API, mediator admin API, sequencer admin API # configs specific for the pulumi project # will be applied to all the stacks in the project pulumiProjectConfig: @@ -236,16 +242,30 @@ cloudArmor: allRulesPreviewOnly: true publicEndpoints: publicScan: - hostname: scan.sv-2.scratcha.global.canton.network.digitalasset.com + hostPrefixRegex: scan pathPrefix: /api/scan - throttleAcrossAllEndpointsAllIps: + throttleAcrossAllEndpointsPerIp: withinIntervalSeconds: 60 - maxRequestsBeforeHttp429: 0 # Keeps scan completely closed to the public + # above the envoy per-IP scan limit (see sv.scan.externalRateLimits.globalPerIpLimits), + # so that envoy stays the primary limiter and this is only a backstop that drops + # abusive sources at the GCP edge + maxRequestsBeforeHttp429: 2000 tokenRegistry: + # the token registry is served off the scan hosts + hostPrefixRegex: scan pathPrefix: /registry - throttleAcrossAllEndpointsAllIps: + throttleAcrossAllEndpointsPerIp: withinIntervalSeconds: 60 - maxRequestsBeforeHttp429: 200 + # above the largest per-endpoint envoy perIpLimits under /registry, with room for + # a client legitimately using several registry endpoints at once + maxRequestsBeforeHttp429: 1000 + sequencer: + hostPrefixRegex: sequencer-[0-9]+ + pathPrefix: / + restrictToRateLimitedPaths: false + throttleAcrossAllEndpointsPerIp: + withinIntervalSeconds: 60 + maxRequestsBeforeHttp429: 20000 multiValidator: postgresPvcSize: '100Gi' resources: @@ -269,11 +289,19 @@ multiValidator: sv: scan: externalRateLimits: - !include(./rate-limits/v0-acs.yaml;./rate-limits/unlimited.yaml;./rate-limits/public-banned.yaml;./rate-limits/token-registry.yaml) + !include(./rate-limits/token-registry.yaml) globalLimits: - maxTokens: 2147483647 - tokensPerFill: 2147483647 + maxTokens: 10000 + tokensPerFill: 10000 + fillInterval: 60s + globalPerIpLimits: + maxTokens: 1000 + tokensPerFill: 1000 fillInterval: 60s + synchronizer: + sequencer: + externalRateLimits: + !include(./rate-limits/sequencer-public-api.yaml) validators: validator-runbook: namespace: validator diff --git a/cluster/configs/shared/rate-limits/public-banned.yaml b/cluster/configs/shared/rate-limits/public-banned.yaml deleted file mode 100644 index d5fad0e007c..00000000000 --- a/cluster/configs/shared/rate-limits/public-banned.yaml +++ /dev/null @@ -1,10 +0,0 @@ -rateLimits: - # banned because deprecated - /api/scan/v1/updates: - name: v1-updates - type: banned - - # banned because deprecated - /api/scan/v0/amulet-config-for-round: - name: amulet-config-for-round - type: banned diff --git a/cluster/configs/shared/rate-limits/sequencer-public-api.yaml b/cluster/configs/shared/rate-limits/sequencer-public-api.yaml new file mode 100644 index 00000000000..54d9d6d4eba --- /dev/null +++ b/cluster/configs/shared/rate-limits/sequencer-public-api.yaml @@ -0,0 +1,56 @@ +globalLimits: + maxTokens: 200000 + tokensPerFill: 200000 + fillInterval: 60s +globalPerIpLimits: + maxTokens: 10000 + tokensPerFill: 10000 + fillInterval: 60s +rateLimits: + # Cheap, infrequent handshake/version negotiation calls. + /com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/: + name: sequencer-connect + type: limited + maxTokens: 10000 + tokensPerFill: 10000 + fillInterval: 60s + perIpLimits: + maxTokens: 100 + tokensPerFill: 100 + fillInterval: 60s + + # Token challenge/refresh. Tokens are long lived, so clients hit this rarely. + /com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/: + name: sequencer-authentication + type: limited + maxTokens: 10000 + tokensPerFill: 10000 + fillInterval: 60s + perIpLimits: + maxTokens: 100 + tokensPerFill: 100 + fillInterval: 60s + + # Subscriptions are long lived streams; only opening one consumes a token. + /com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe: + name: sequencer-subscribe + type: limited + maxTokens: 10000 + tokensPerFill: 10000 + fillInterval: 60s + perIpLimits: + maxTokens: 100 + tokensPerFill: 100 + fillInterval: 60s + + # Onboarding only. Also covers DownloadTopologyStateForInitHash. + /com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit: + name: sequencer-download-topology-state-for-init + type: limited + maxTokens: 600 + tokensPerFill: 600 + fillInterval: 60s + perIpLimits: + maxTokens: 20 + tokensPerFill: 20 + fillInterval: 60s diff --git a/cluster/configs/shared/rate-limits/unlimited.yaml b/cluster/configs/shared/rate-limits/unlimited.yaml deleted file mode 100644 index 6d9cd05373b..00000000000 --- a/cluster/configs/shared/rate-limits/unlimited.yaml +++ /dev/null @@ -1,216 +0,0 @@ -rateLimits: - # Health/status endpoints (unlimited) - /api/scan/readyz: - name: readyz - type: unlimited - - /api/scan/livez: - name: livez - type: unlimited - - /api/scan/status: - name: status - type: unlimited - - /api/scan/version: - name: version - type: unlimited - - # External API endpoints (unlimited) - /api/scan/v0/dso: - name: dso-info - type: unlimited - - /api/scan/v0/validators: - name: validators - type: unlimited - - /api/scan/v0/scans: - name: scans - type: unlimited - - /api/scan/v0/dso-sequencers: - name: dso-sequencers - type: unlimited - - /api/scan/v0/sv-bft-sequencers: - name: bft-sequencers - type: unlimited - - /api/scan/v0/domains: - name: domains - type: unlimited - - /api/scan/v1/domains: - name: domains-v1 - type: unlimited - - /api/scan/v0/closed-rounds: - name: closed-rounds - type: unlimited - - /api/scan/v0/open-and-issuing-mining-rounds: - name: open-and-issuing-mining-rounds - type: unlimited - - /api/scan/v0/updates: - name: v0-updates - type: unlimited - - /api/scan/v2/updates: - name: v2-updates - type: unlimited - - /api/scan/v0/state: - name: state - type: unlimited - - /api/scan/v1/state: - name: state - type: unlimited - - /api/scan/v2/state: - name: state - type: unlimited - - /api/scan/v0/holdings: - name: holdings - type: unlimited - - /api/scan/v1/holdings: - name: holdings - type: unlimited - - /api/scan/v2/holdings: - name: holdings - type: unlimited - - /api/scan/v0/ans-entries: - name: ans-entries - type: unlimited - - /api/scan/v0/dso-party-id: - name: dso-party-id - type: unlimited - - /api/scan/v0/backfilling: - name: backfilling - type: unlimited - - /api/scan/v0/admin/validator: - name: validator-licenses - type: unlimited - - /api/scan/v0/admin/sv/voterequests: - name: sv-voterequests - type: unlimited - - /api/scan/v0/admin/sv/voteresults: - name: sv-voteresults - type: unlimited - - /api/scan/v0/admin/sv/previous-sv-reward-weight: - name: sv-previous-sv-reward-weight - type: unlimited - - /api/scan/v0/amulet-price: - name: amulet-price-votes - type: unlimited - - /api/scan/v0/amulet-rules: - name: amulet-rules - type: unlimited - - /api/scan/v0/ans-rules: - name: ans-rules - type: unlimited - - /api/scan/v0/events: - name: events - type: unlimited - - /api/scan/v0/external-party-amulet-rules: - name: external-party-amulet-rules - type: unlimited - - /api/scan/v0/feature-support: - name: feature-support - type: unlimited - - /api/scan/v0/featured-apps: - name: featured-apps - type: unlimited - - /api/scan/v0/migrations: - name: migrations-schedule - type: unlimited - - /api/scan/v0/splice-instance-names: - name: splice-instance-names - type: unlimited - - /api/scan/v0/synchronizer-bootstrapping-transactions: - name: synchronizer-bootstrapping-transactions - type: unlimited - - /api/scan/v0/synchronizer-identities: - name: synchronizer-identities - type: unlimited - - /api/scan/v0/transfer-command-counter: - name: transfer-command-counter - type: unlimited - - /api/scan/v0/transfer-command: - name: transfer-command-status - type: unlimited - - /api/scan/v0/transfer-preapprovals: - name: transfer-preapprovals - type: unlimited - - /api/scan/v0/unclaimed-development-fund-coupons: - name: unclaimed-development-fund-coupons - type: unlimited - - /api/scan/v0/voterequest: - name: voterequest - type: unlimited - - /api/scan/v0/voterequests: - name: voterequests - type: unlimited - - /api/scan/v0/history/bulk/acs: - name: listBulkAcsSnapshotObjects - type: unlimited - - /api/scan/v0/history/bulk/updates: - name: listBulkUpdateHistoryObjects - type: unlimited - - /api/scan/v0/history/bulk/checksums: - name: getBulkHistoryChecksums - type: unlimited - - /api/scan/v0/active-synchronizer-serial: - name: activeSynchronizerSerial - type: unlimited - - /api/scan/v0/roll-forward-lsu: - name: rollForwardLsu - type: unlimited - - /api/scan/v0/lsu: - name: lsu - type: unlimited - - /api/scan/v0/internal/reward-accounting-process: - name: reward-accounting-process - type: unlimited - - # TODO(#3429): This currently fails with "Orphaned rate limit prefixes not matching any scan.yaml endpoint", - # because the endpoint is in scan-stream-server.yaml, not in scan.yaml - # /api/scan/v0/history/bulk/download: - # name: bulkStorageDownload - # type: unlimited diff --git a/cluster/configs/shared/rate-limits/v0-acs.yaml b/cluster/configs/shared/rate-limits/v0-acs.yaml deleted file mode 100644 index af956dbdb22..00000000000 --- a/cluster/configs/shared/rate-limits/v0-acs.yaml +++ /dev/null @@ -1,11 +0,0 @@ -rateLimits: - /api/scan/v0/acs: - name: acs - type: limited - maxTokens: 500 # The per-endpoint global limit (shared between all ips) - tokensPerFill: 500 - fillInterval: 60s - perIpLimits: # per-IP limit - maxTokens: 10 - tokensPerFill: 5 - fillInterval: 60s diff --git a/cluster/configs/shared/scratch-default-synchronizer-migration.yaml b/cluster/configs/shared/scratch-default-synchronizer-migration.yaml index 814030e96cc..d84bb786400 100644 --- a/cluster/configs/shared/scratch-default-synchronizer-migration.yaml +++ b/cluster/configs/shared/scratch-default-synchronizer-migration.yaml @@ -1,9 +1,10 @@ synchronizerMigration: - splitSvDeploymentEnabled: true frozenMigrationId: 0 active: id: 0 version: "local" + sequencer: + enableBftSequencer: true releaseReference: gitReference: "refs/heads/main" repoUrl: https://github.com/canton-network/splice diff --git a/cluster/configs/shared/scratchnet.yaml b/cluster/configs/shared/scratchnet.yaml index 1438dce184f..36009515040 100644 --- a/cluster/configs/shared/scratchnet.yaml +++ b/cluster/configs/shared/scratchnet.yaml @@ -10,6 +10,7 @@ infra: retentionDuration: "30d" retentionSize: "200GB" storageSize: "600Gi" + enableSweetSecurity: true pulumiProjectConfig: default: cloudSql: diff --git a/cluster/deployment/mock/config.yaml b/cluster/deployment/mock/config.yaml index 71b301eb917..de24a956177 100644 --- a/cluster/deployment/mock/config.yaml +++ b/cluster/deployment/mock/config.yaml @@ -57,7 +57,6 @@ splitwell: maxDuration: "5m" retention: "30d" synchronizerMigration: - splitSvDeploymentEnabled: true frozenMigrationId: 2 archived: - id: 5 diff --git a/cluster/deployment/scratchneta/config.resolved.yaml b/cluster/deployment/scratchneta/config.resolved.yaml index aa126001187..557c166e83e 100644 --- a/cluster/deployment/scratchneta/config.resolved.yaml +++ b/cluster/deployment/scratchneta/config.resolved.yaml @@ -4,15 +4,23 @@ cloudArmor: enabled: false publicEndpoints: publicScan: - hostname: 'scan.sv-2.scratcha.global.canton.network.digitalasset.com' + hostPrefixRegex: 'scan' pathPrefix: '/api/scan' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 0 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 2000 + withinIntervalSeconds: 60 + sequencer: + hostPrefixRegex: 'sequencer-[0-9]+' + pathPrefix: '/' + restrictToRateLimitedPaths: false + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 20000 withinIntervalSeconds: 60 tokenRegistry: + hostPrefixRegex: 'scan' pathPrefix: '/registry' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 200 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 1000 withinIntervalSeconds: 60 cluster: nodePools: @@ -26,14 +34,27 @@ cluster: minNodes: 1 nodeType: 'n4d-standard-8' infra: + enableSweetSecurity: true gkeGateway: proxyForIstioHttp: false istio: enableIngressAccessLogging: true enablePublicTokenRegistry: true - sequencerFlowControl: - initialConnectionWindowSize: 52428800 - initialStreamWindowSize: 524288 + flowControl: + internal: + initialConnectionWindowSize: 104857600 + initialStreamWindowSize: 1048576 + ports: + - 5001 + - 5002 + - 5007 + - 5009 + public: + initialConnectionWindowSize: 52428800 + initialStreamWindowSize: 524288 + ports: + - 5008 + - 5010 prometheus: retentionDuration: '30d' retentionSize: '200GB' @@ -205,181 +226,13 @@ sv: externalRateLimits: globalLimits: fillInterval: '60s' - maxTokens: 2147483647 - tokensPerFill: 2147483647 + maxTokens: 10000 + tokensPerFill: 10000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 1000 + tokensPerFill: 1000 rateLimits: - /api/scan/livez: - name: 'livez' - type: 'unlimited' - /api/scan/readyz: - name: 'readyz' - type: 'unlimited' - /api/scan/status: - name: 'status' - type: 'unlimited' - /api/scan/v0/acs: - fillInterval: '60s' - maxTokens: 500 - name: 'acs' - perIpLimits: - fillInterval: '60s' - maxTokens: 10 - tokensPerFill: 5 - tokensPerFill: 500 - type: 'limited' - /api/scan/v0/active-synchronizer-serial: - name: 'activeSynchronizerSerial' - type: 'unlimited' - /api/scan/v0/admin/sv/previous-sv-reward-weight: - name: 'sv-previous-sv-reward-weight' - type: 'unlimited' - /api/scan/v0/admin/sv/voterequests: - name: 'sv-voterequests' - type: 'unlimited' - /api/scan/v0/admin/sv/voteresults: - name: 'sv-voteresults' - type: 'unlimited' - /api/scan/v0/admin/validator: - name: 'validator-licenses' - type: 'unlimited' - /api/scan/v0/amulet-config-for-round: - name: 'amulet-config-for-round' - type: 'banned' - /api/scan/v0/amulet-price: - name: 'amulet-price-votes' - type: 'unlimited' - /api/scan/v0/amulet-rules: - name: 'amulet-rules' - type: 'unlimited' - /api/scan/v0/ans-entries: - name: 'ans-entries' - type: 'unlimited' - /api/scan/v0/ans-rules: - name: 'ans-rules' - type: 'unlimited' - /api/scan/v0/backfilling: - name: 'backfilling' - type: 'unlimited' - /api/scan/v0/closed-rounds: - name: 'closed-rounds' - type: 'unlimited' - /api/scan/v0/domains: - name: 'domains' - type: 'unlimited' - /api/scan/v0/dso: - name: 'dso-info' - type: 'unlimited' - /api/scan/v0/dso-party-id: - name: 'dso-party-id' - type: 'unlimited' - /api/scan/v0/dso-sequencers: - name: 'dso-sequencers' - type: 'unlimited' - /api/scan/v0/events: - name: 'events' - type: 'unlimited' - /api/scan/v0/external-party-amulet-rules: - name: 'external-party-amulet-rules' - type: 'unlimited' - /api/scan/v0/feature-support: - name: 'feature-support' - type: 'unlimited' - /api/scan/v0/featured-apps: - name: 'featured-apps' - type: 'unlimited' - /api/scan/v0/history/bulk/acs: - name: 'listBulkAcsSnapshotObjects' - type: 'unlimited' - /api/scan/v0/history/bulk/checksums: - name: 'getBulkHistoryChecksums' - type: 'unlimited' - /api/scan/v0/history/bulk/updates: - name: 'listBulkUpdateHistoryObjects' - type: 'unlimited' - /api/scan/v0/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v0/internal/reward-accounting-process: - name: 'reward-accounting-process' - type: 'unlimited' - /api/scan/v0/lsu: - name: 'lsu' - type: 'unlimited' - /api/scan/v0/migrations: - name: 'migrations-schedule' - type: 'unlimited' - /api/scan/v0/open-and-issuing-mining-rounds: - name: 'open-and-issuing-mining-rounds' - type: 'unlimited' - /api/scan/v0/roll-forward-lsu: - name: 'rollForwardLsu' - type: 'unlimited' - /api/scan/v0/scans: - name: 'scans' - type: 'unlimited' - /api/scan/v0/splice-instance-names: - name: 'splice-instance-names' - type: 'unlimited' - /api/scan/v0/state: - name: 'state' - type: 'unlimited' - /api/scan/v0/sv-bft-sequencers: - name: 'bft-sequencers' - type: 'unlimited' - /api/scan/v0/synchronizer-bootstrapping-transactions: - name: 'synchronizer-bootstrapping-transactions' - type: 'unlimited' - /api/scan/v0/synchronizer-identities: - name: 'synchronizer-identities' - type: 'unlimited' - /api/scan/v0/transfer-command: - name: 'transfer-command-status' - type: 'unlimited' - /api/scan/v0/transfer-command-counter: - name: 'transfer-command-counter' - type: 'unlimited' - /api/scan/v0/transfer-preapprovals: - name: 'transfer-preapprovals' - type: 'unlimited' - /api/scan/v0/unclaimed-development-fund-coupons: - name: 'unclaimed-development-fund-coupons' - type: 'unlimited' - /api/scan/v0/updates: - name: 'v0-updates' - type: 'unlimited' - /api/scan/v0/validators: - name: 'validators' - type: 'unlimited' - /api/scan/v0/voterequest: - name: 'voterequest' - type: 'unlimited' - /api/scan/v0/voterequests: - name: 'voterequests' - type: 'unlimited' - /api/scan/v1/domains: - name: 'domains-v1' - type: 'unlimited' - /api/scan/v1/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v1/state: - name: 'state' - type: 'unlimited' - /api/scan/v1/updates: - name: 'v1-updates' - type: 'banned' - /api/scan/v2/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v2/state: - name: 'state' - type: 'unlimited' - /api/scan/v2/updates: - name: 'v2-updates' - type: 'unlimited' - /api/scan/version: - name: 'version' - type: 'unlimited' /registry/allocation-instruction/v1/allocation-factory: fillInterval: '60s' maxTokens: 720 @@ -500,6 +353,58 @@ sv: tokensPerFill: 120 tokensPerFill: 720 type: 'limited' + synchronizer: + sequencer: + externalRateLimits: + globalLimits: + fillInterval: '60s' + maxTokens: 200000 + tokensPerFill: 200000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 10000 + tokensPerFill: 10000 + rateLimits: + /com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-authentication' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-connect' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit: + fillInterval: '60s' + maxTokens: 600 + name: 'sequencer-download-topology-state-for-init' + perIpLimits: + fillInterval: '60s' + maxTokens: 20 + tokensPerFill: 20 + tokensPerFill: 600 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-subscribe' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' svs: sv: cometbft: @@ -1872,9 +1777,10 @@ synchronizerMigration: pulumiStacksDir: 'cluster/stacks/prod' repoUrl: 'https://github.com/canton-network/splice' spliceRoot: 'splice' + sequencer: + enableBftSequencer: true version: 'local' frozenMigrationId: 0 - splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetb/config.resolved.yaml b/cluster/deployment/scratchnetb/config.resolved.yaml index aa126001187..557c166e83e 100644 --- a/cluster/deployment/scratchnetb/config.resolved.yaml +++ b/cluster/deployment/scratchnetb/config.resolved.yaml @@ -4,15 +4,23 @@ cloudArmor: enabled: false publicEndpoints: publicScan: - hostname: 'scan.sv-2.scratcha.global.canton.network.digitalasset.com' + hostPrefixRegex: 'scan' pathPrefix: '/api/scan' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 0 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 2000 + withinIntervalSeconds: 60 + sequencer: + hostPrefixRegex: 'sequencer-[0-9]+' + pathPrefix: '/' + restrictToRateLimitedPaths: false + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 20000 withinIntervalSeconds: 60 tokenRegistry: + hostPrefixRegex: 'scan' pathPrefix: '/registry' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 200 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 1000 withinIntervalSeconds: 60 cluster: nodePools: @@ -26,14 +34,27 @@ cluster: minNodes: 1 nodeType: 'n4d-standard-8' infra: + enableSweetSecurity: true gkeGateway: proxyForIstioHttp: false istio: enableIngressAccessLogging: true enablePublicTokenRegistry: true - sequencerFlowControl: - initialConnectionWindowSize: 52428800 - initialStreamWindowSize: 524288 + flowControl: + internal: + initialConnectionWindowSize: 104857600 + initialStreamWindowSize: 1048576 + ports: + - 5001 + - 5002 + - 5007 + - 5009 + public: + initialConnectionWindowSize: 52428800 + initialStreamWindowSize: 524288 + ports: + - 5008 + - 5010 prometheus: retentionDuration: '30d' retentionSize: '200GB' @@ -205,181 +226,13 @@ sv: externalRateLimits: globalLimits: fillInterval: '60s' - maxTokens: 2147483647 - tokensPerFill: 2147483647 + maxTokens: 10000 + tokensPerFill: 10000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 1000 + tokensPerFill: 1000 rateLimits: - /api/scan/livez: - name: 'livez' - type: 'unlimited' - /api/scan/readyz: - name: 'readyz' - type: 'unlimited' - /api/scan/status: - name: 'status' - type: 'unlimited' - /api/scan/v0/acs: - fillInterval: '60s' - maxTokens: 500 - name: 'acs' - perIpLimits: - fillInterval: '60s' - maxTokens: 10 - tokensPerFill: 5 - tokensPerFill: 500 - type: 'limited' - /api/scan/v0/active-synchronizer-serial: - name: 'activeSynchronizerSerial' - type: 'unlimited' - /api/scan/v0/admin/sv/previous-sv-reward-weight: - name: 'sv-previous-sv-reward-weight' - type: 'unlimited' - /api/scan/v0/admin/sv/voterequests: - name: 'sv-voterequests' - type: 'unlimited' - /api/scan/v0/admin/sv/voteresults: - name: 'sv-voteresults' - type: 'unlimited' - /api/scan/v0/admin/validator: - name: 'validator-licenses' - type: 'unlimited' - /api/scan/v0/amulet-config-for-round: - name: 'amulet-config-for-round' - type: 'banned' - /api/scan/v0/amulet-price: - name: 'amulet-price-votes' - type: 'unlimited' - /api/scan/v0/amulet-rules: - name: 'amulet-rules' - type: 'unlimited' - /api/scan/v0/ans-entries: - name: 'ans-entries' - type: 'unlimited' - /api/scan/v0/ans-rules: - name: 'ans-rules' - type: 'unlimited' - /api/scan/v0/backfilling: - name: 'backfilling' - type: 'unlimited' - /api/scan/v0/closed-rounds: - name: 'closed-rounds' - type: 'unlimited' - /api/scan/v0/domains: - name: 'domains' - type: 'unlimited' - /api/scan/v0/dso: - name: 'dso-info' - type: 'unlimited' - /api/scan/v0/dso-party-id: - name: 'dso-party-id' - type: 'unlimited' - /api/scan/v0/dso-sequencers: - name: 'dso-sequencers' - type: 'unlimited' - /api/scan/v0/events: - name: 'events' - type: 'unlimited' - /api/scan/v0/external-party-amulet-rules: - name: 'external-party-amulet-rules' - type: 'unlimited' - /api/scan/v0/feature-support: - name: 'feature-support' - type: 'unlimited' - /api/scan/v0/featured-apps: - name: 'featured-apps' - type: 'unlimited' - /api/scan/v0/history/bulk/acs: - name: 'listBulkAcsSnapshotObjects' - type: 'unlimited' - /api/scan/v0/history/bulk/checksums: - name: 'getBulkHistoryChecksums' - type: 'unlimited' - /api/scan/v0/history/bulk/updates: - name: 'listBulkUpdateHistoryObjects' - type: 'unlimited' - /api/scan/v0/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v0/internal/reward-accounting-process: - name: 'reward-accounting-process' - type: 'unlimited' - /api/scan/v0/lsu: - name: 'lsu' - type: 'unlimited' - /api/scan/v0/migrations: - name: 'migrations-schedule' - type: 'unlimited' - /api/scan/v0/open-and-issuing-mining-rounds: - name: 'open-and-issuing-mining-rounds' - type: 'unlimited' - /api/scan/v0/roll-forward-lsu: - name: 'rollForwardLsu' - type: 'unlimited' - /api/scan/v0/scans: - name: 'scans' - type: 'unlimited' - /api/scan/v0/splice-instance-names: - name: 'splice-instance-names' - type: 'unlimited' - /api/scan/v0/state: - name: 'state' - type: 'unlimited' - /api/scan/v0/sv-bft-sequencers: - name: 'bft-sequencers' - type: 'unlimited' - /api/scan/v0/synchronizer-bootstrapping-transactions: - name: 'synchronizer-bootstrapping-transactions' - type: 'unlimited' - /api/scan/v0/synchronizer-identities: - name: 'synchronizer-identities' - type: 'unlimited' - /api/scan/v0/transfer-command: - name: 'transfer-command-status' - type: 'unlimited' - /api/scan/v0/transfer-command-counter: - name: 'transfer-command-counter' - type: 'unlimited' - /api/scan/v0/transfer-preapprovals: - name: 'transfer-preapprovals' - type: 'unlimited' - /api/scan/v0/unclaimed-development-fund-coupons: - name: 'unclaimed-development-fund-coupons' - type: 'unlimited' - /api/scan/v0/updates: - name: 'v0-updates' - type: 'unlimited' - /api/scan/v0/validators: - name: 'validators' - type: 'unlimited' - /api/scan/v0/voterequest: - name: 'voterequest' - type: 'unlimited' - /api/scan/v0/voterequests: - name: 'voterequests' - type: 'unlimited' - /api/scan/v1/domains: - name: 'domains-v1' - type: 'unlimited' - /api/scan/v1/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v1/state: - name: 'state' - type: 'unlimited' - /api/scan/v1/updates: - name: 'v1-updates' - type: 'banned' - /api/scan/v2/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v2/state: - name: 'state' - type: 'unlimited' - /api/scan/v2/updates: - name: 'v2-updates' - type: 'unlimited' - /api/scan/version: - name: 'version' - type: 'unlimited' /registry/allocation-instruction/v1/allocation-factory: fillInterval: '60s' maxTokens: 720 @@ -500,6 +353,58 @@ sv: tokensPerFill: 120 tokensPerFill: 720 type: 'limited' + synchronizer: + sequencer: + externalRateLimits: + globalLimits: + fillInterval: '60s' + maxTokens: 200000 + tokensPerFill: 200000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 10000 + tokensPerFill: 10000 + rateLimits: + /com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-authentication' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-connect' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit: + fillInterval: '60s' + maxTokens: 600 + name: 'sequencer-download-topology-state-for-init' + perIpLimits: + fillInterval: '60s' + maxTokens: 20 + tokensPerFill: 20 + tokensPerFill: 600 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-subscribe' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' svs: sv: cometbft: @@ -1872,9 +1777,10 @@ synchronizerMigration: pulumiStacksDir: 'cluster/stacks/prod' repoUrl: 'https://github.com/canton-network/splice' spliceRoot: 'splice' + sequencer: + enableBftSequencer: true version: 'local' frozenMigrationId: 0 - splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetc/config.resolved.yaml b/cluster/deployment/scratchnetc/config.resolved.yaml index aa126001187..557c166e83e 100644 --- a/cluster/deployment/scratchnetc/config.resolved.yaml +++ b/cluster/deployment/scratchnetc/config.resolved.yaml @@ -4,15 +4,23 @@ cloudArmor: enabled: false publicEndpoints: publicScan: - hostname: 'scan.sv-2.scratcha.global.canton.network.digitalasset.com' + hostPrefixRegex: 'scan' pathPrefix: '/api/scan' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 0 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 2000 + withinIntervalSeconds: 60 + sequencer: + hostPrefixRegex: 'sequencer-[0-9]+' + pathPrefix: '/' + restrictToRateLimitedPaths: false + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 20000 withinIntervalSeconds: 60 tokenRegistry: + hostPrefixRegex: 'scan' pathPrefix: '/registry' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 200 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 1000 withinIntervalSeconds: 60 cluster: nodePools: @@ -26,14 +34,27 @@ cluster: minNodes: 1 nodeType: 'n4d-standard-8' infra: + enableSweetSecurity: true gkeGateway: proxyForIstioHttp: false istio: enableIngressAccessLogging: true enablePublicTokenRegistry: true - sequencerFlowControl: - initialConnectionWindowSize: 52428800 - initialStreamWindowSize: 524288 + flowControl: + internal: + initialConnectionWindowSize: 104857600 + initialStreamWindowSize: 1048576 + ports: + - 5001 + - 5002 + - 5007 + - 5009 + public: + initialConnectionWindowSize: 52428800 + initialStreamWindowSize: 524288 + ports: + - 5008 + - 5010 prometheus: retentionDuration: '30d' retentionSize: '200GB' @@ -205,181 +226,13 @@ sv: externalRateLimits: globalLimits: fillInterval: '60s' - maxTokens: 2147483647 - tokensPerFill: 2147483647 + maxTokens: 10000 + tokensPerFill: 10000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 1000 + tokensPerFill: 1000 rateLimits: - /api/scan/livez: - name: 'livez' - type: 'unlimited' - /api/scan/readyz: - name: 'readyz' - type: 'unlimited' - /api/scan/status: - name: 'status' - type: 'unlimited' - /api/scan/v0/acs: - fillInterval: '60s' - maxTokens: 500 - name: 'acs' - perIpLimits: - fillInterval: '60s' - maxTokens: 10 - tokensPerFill: 5 - tokensPerFill: 500 - type: 'limited' - /api/scan/v0/active-synchronizer-serial: - name: 'activeSynchronizerSerial' - type: 'unlimited' - /api/scan/v0/admin/sv/previous-sv-reward-weight: - name: 'sv-previous-sv-reward-weight' - type: 'unlimited' - /api/scan/v0/admin/sv/voterequests: - name: 'sv-voterequests' - type: 'unlimited' - /api/scan/v0/admin/sv/voteresults: - name: 'sv-voteresults' - type: 'unlimited' - /api/scan/v0/admin/validator: - name: 'validator-licenses' - type: 'unlimited' - /api/scan/v0/amulet-config-for-round: - name: 'amulet-config-for-round' - type: 'banned' - /api/scan/v0/amulet-price: - name: 'amulet-price-votes' - type: 'unlimited' - /api/scan/v0/amulet-rules: - name: 'amulet-rules' - type: 'unlimited' - /api/scan/v0/ans-entries: - name: 'ans-entries' - type: 'unlimited' - /api/scan/v0/ans-rules: - name: 'ans-rules' - type: 'unlimited' - /api/scan/v0/backfilling: - name: 'backfilling' - type: 'unlimited' - /api/scan/v0/closed-rounds: - name: 'closed-rounds' - type: 'unlimited' - /api/scan/v0/domains: - name: 'domains' - type: 'unlimited' - /api/scan/v0/dso: - name: 'dso-info' - type: 'unlimited' - /api/scan/v0/dso-party-id: - name: 'dso-party-id' - type: 'unlimited' - /api/scan/v0/dso-sequencers: - name: 'dso-sequencers' - type: 'unlimited' - /api/scan/v0/events: - name: 'events' - type: 'unlimited' - /api/scan/v0/external-party-amulet-rules: - name: 'external-party-amulet-rules' - type: 'unlimited' - /api/scan/v0/feature-support: - name: 'feature-support' - type: 'unlimited' - /api/scan/v0/featured-apps: - name: 'featured-apps' - type: 'unlimited' - /api/scan/v0/history/bulk/acs: - name: 'listBulkAcsSnapshotObjects' - type: 'unlimited' - /api/scan/v0/history/bulk/checksums: - name: 'getBulkHistoryChecksums' - type: 'unlimited' - /api/scan/v0/history/bulk/updates: - name: 'listBulkUpdateHistoryObjects' - type: 'unlimited' - /api/scan/v0/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v0/internal/reward-accounting-process: - name: 'reward-accounting-process' - type: 'unlimited' - /api/scan/v0/lsu: - name: 'lsu' - type: 'unlimited' - /api/scan/v0/migrations: - name: 'migrations-schedule' - type: 'unlimited' - /api/scan/v0/open-and-issuing-mining-rounds: - name: 'open-and-issuing-mining-rounds' - type: 'unlimited' - /api/scan/v0/roll-forward-lsu: - name: 'rollForwardLsu' - type: 'unlimited' - /api/scan/v0/scans: - name: 'scans' - type: 'unlimited' - /api/scan/v0/splice-instance-names: - name: 'splice-instance-names' - type: 'unlimited' - /api/scan/v0/state: - name: 'state' - type: 'unlimited' - /api/scan/v0/sv-bft-sequencers: - name: 'bft-sequencers' - type: 'unlimited' - /api/scan/v0/synchronizer-bootstrapping-transactions: - name: 'synchronizer-bootstrapping-transactions' - type: 'unlimited' - /api/scan/v0/synchronizer-identities: - name: 'synchronizer-identities' - type: 'unlimited' - /api/scan/v0/transfer-command: - name: 'transfer-command-status' - type: 'unlimited' - /api/scan/v0/transfer-command-counter: - name: 'transfer-command-counter' - type: 'unlimited' - /api/scan/v0/transfer-preapprovals: - name: 'transfer-preapprovals' - type: 'unlimited' - /api/scan/v0/unclaimed-development-fund-coupons: - name: 'unclaimed-development-fund-coupons' - type: 'unlimited' - /api/scan/v0/updates: - name: 'v0-updates' - type: 'unlimited' - /api/scan/v0/validators: - name: 'validators' - type: 'unlimited' - /api/scan/v0/voterequest: - name: 'voterequest' - type: 'unlimited' - /api/scan/v0/voterequests: - name: 'voterequests' - type: 'unlimited' - /api/scan/v1/domains: - name: 'domains-v1' - type: 'unlimited' - /api/scan/v1/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v1/state: - name: 'state' - type: 'unlimited' - /api/scan/v1/updates: - name: 'v1-updates' - type: 'banned' - /api/scan/v2/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v2/state: - name: 'state' - type: 'unlimited' - /api/scan/v2/updates: - name: 'v2-updates' - type: 'unlimited' - /api/scan/version: - name: 'version' - type: 'unlimited' /registry/allocation-instruction/v1/allocation-factory: fillInterval: '60s' maxTokens: 720 @@ -500,6 +353,58 @@ sv: tokensPerFill: 120 tokensPerFill: 720 type: 'limited' + synchronizer: + sequencer: + externalRateLimits: + globalLimits: + fillInterval: '60s' + maxTokens: 200000 + tokensPerFill: 200000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 10000 + tokensPerFill: 10000 + rateLimits: + /com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-authentication' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-connect' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit: + fillInterval: '60s' + maxTokens: 600 + name: 'sequencer-download-topology-state-for-init' + perIpLimits: + fillInterval: '60s' + maxTokens: 20 + tokensPerFill: 20 + tokensPerFill: 600 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-subscribe' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' svs: sv: cometbft: @@ -1872,9 +1777,10 @@ synchronizerMigration: pulumiStacksDir: 'cluster/stacks/prod' repoUrl: 'https://github.com/canton-network/splice' spliceRoot: 'splice' + sequencer: + enableBftSequencer: true version: 'local' frozenMigrationId: 0 - splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetd/.envrc.vars b/cluster/deployment/scratchnetd/.envrc.vars index 98446ba0ec5..ce3f6cc5dd9 100644 --- a/cluster/deployment/scratchnetd/.envrc.vars +++ b/cluster/deployment/scratchnetd/.envrc.vars @@ -34,3 +34,4 @@ export APPROVE_SV_RUNBOOK=true export CACHE_DEV_DOCKER_REGISTRY=$DEV_DOCKER_REGISTRY export SELF_HOSTED_FLUENT_BIT=true +export SPLICE_DEPLOY_SV_RUNBOOK=true diff --git a/cluster/deployment/scratchnetd/config.resolved.yaml b/cluster/deployment/scratchnetd/config.resolved.yaml index aa126001187..d7f86cb0edb 100644 --- a/cluster/deployment/scratchnetd/config.resolved.yaml +++ b/cluster/deployment/scratchnetd/config.resolved.yaml @@ -1,18 +1,28 @@ # This file is generated automatically. Do not edit directly. cloudArmor: - allRulesPreviewOnly: true - enabled: false + allRulesPreviewOnly: false + enabled: true + logging: + enabled: true publicEndpoints: publicScan: - hostname: 'scan.sv-2.scratcha.global.canton.network.digitalasset.com' + hostPrefixRegex: 'scan' pathPrefix: '/api/scan' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 0 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 2000 + withinIntervalSeconds: 60 + sequencer: + hostPrefixRegex: 'sequencer-[0-9]+' + pathPrefix: '/' + restrictToRateLimitedPaths: false + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 20000 withinIntervalSeconds: 60 tokenRegistry: + hostPrefixRegex: 'scan' pathPrefix: '/registry' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 200 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 1000 withinIntervalSeconds: 60 cluster: nodePools: @@ -26,14 +36,27 @@ cluster: minNodes: 1 nodeType: 'n4d-standard-8' infra: + enableSweetSecurity: true gkeGateway: - proxyForIstioHttp: false + proxyForIstioHttp: true istio: enableIngressAccessLogging: true enablePublicTokenRegistry: true - sequencerFlowControl: - initialConnectionWindowSize: 52428800 - initialStreamWindowSize: 524288 + flowControl: + internal: + initialConnectionWindowSize: 104857600 + initialStreamWindowSize: 1048576 + ports: + - 5001 + - 5002 + - 5007 + - 5009 + public: + initialConnectionWindowSize: 52428800 + initialStreamWindowSize: 524288 + ports: + - 5008 + - 5010 prometheus: retentionDuration: '30d' retentionSize: '200GB' @@ -205,181 +228,13 @@ sv: externalRateLimits: globalLimits: fillInterval: '60s' - maxTokens: 2147483647 - tokensPerFill: 2147483647 + maxTokens: 10000 + tokensPerFill: 10000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 1000 + tokensPerFill: 1000 rateLimits: - /api/scan/livez: - name: 'livez' - type: 'unlimited' - /api/scan/readyz: - name: 'readyz' - type: 'unlimited' - /api/scan/status: - name: 'status' - type: 'unlimited' - /api/scan/v0/acs: - fillInterval: '60s' - maxTokens: 500 - name: 'acs' - perIpLimits: - fillInterval: '60s' - maxTokens: 10 - tokensPerFill: 5 - tokensPerFill: 500 - type: 'limited' - /api/scan/v0/active-synchronizer-serial: - name: 'activeSynchronizerSerial' - type: 'unlimited' - /api/scan/v0/admin/sv/previous-sv-reward-weight: - name: 'sv-previous-sv-reward-weight' - type: 'unlimited' - /api/scan/v0/admin/sv/voterequests: - name: 'sv-voterequests' - type: 'unlimited' - /api/scan/v0/admin/sv/voteresults: - name: 'sv-voteresults' - type: 'unlimited' - /api/scan/v0/admin/validator: - name: 'validator-licenses' - type: 'unlimited' - /api/scan/v0/amulet-config-for-round: - name: 'amulet-config-for-round' - type: 'banned' - /api/scan/v0/amulet-price: - name: 'amulet-price-votes' - type: 'unlimited' - /api/scan/v0/amulet-rules: - name: 'amulet-rules' - type: 'unlimited' - /api/scan/v0/ans-entries: - name: 'ans-entries' - type: 'unlimited' - /api/scan/v0/ans-rules: - name: 'ans-rules' - type: 'unlimited' - /api/scan/v0/backfilling: - name: 'backfilling' - type: 'unlimited' - /api/scan/v0/closed-rounds: - name: 'closed-rounds' - type: 'unlimited' - /api/scan/v0/domains: - name: 'domains' - type: 'unlimited' - /api/scan/v0/dso: - name: 'dso-info' - type: 'unlimited' - /api/scan/v0/dso-party-id: - name: 'dso-party-id' - type: 'unlimited' - /api/scan/v0/dso-sequencers: - name: 'dso-sequencers' - type: 'unlimited' - /api/scan/v0/events: - name: 'events' - type: 'unlimited' - /api/scan/v0/external-party-amulet-rules: - name: 'external-party-amulet-rules' - type: 'unlimited' - /api/scan/v0/feature-support: - name: 'feature-support' - type: 'unlimited' - /api/scan/v0/featured-apps: - name: 'featured-apps' - type: 'unlimited' - /api/scan/v0/history/bulk/acs: - name: 'listBulkAcsSnapshotObjects' - type: 'unlimited' - /api/scan/v0/history/bulk/checksums: - name: 'getBulkHistoryChecksums' - type: 'unlimited' - /api/scan/v0/history/bulk/updates: - name: 'listBulkUpdateHistoryObjects' - type: 'unlimited' - /api/scan/v0/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v0/internal/reward-accounting-process: - name: 'reward-accounting-process' - type: 'unlimited' - /api/scan/v0/lsu: - name: 'lsu' - type: 'unlimited' - /api/scan/v0/migrations: - name: 'migrations-schedule' - type: 'unlimited' - /api/scan/v0/open-and-issuing-mining-rounds: - name: 'open-and-issuing-mining-rounds' - type: 'unlimited' - /api/scan/v0/roll-forward-lsu: - name: 'rollForwardLsu' - type: 'unlimited' - /api/scan/v0/scans: - name: 'scans' - type: 'unlimited' - /api/scan/v0/splice-instance-names: - name: 'splice-instance-names' - type: 'unlimited' - /api/scan/v0/state: - name: 'state' - type: 'unlimited' - /api/scan/v0/sv-bft-sequencers: - name: 'bft-sequencers' - type: 'unlimited' - /api/scan/v0/synchronizer-bootstrapping-transactions: - name: 'synchronizer-bootstrapping-transactions' - type: 'unlimited' - /api/scan/v0/synchronizer-identities: - name: 'synchronizer-identities' - type: 'unlimited' - /api/scan/v0/transfer-command: - name: 'transfer-command-status' - type: 'unlimited' - /api/scan/v0/transfer-command-counter: - name: 'transfer-command-counter' - type: 'unlimited' - /api/scan/v0/transfer-preapprovals: - name: 'transfer-preapprovals' - type: 'unlimited' - /api/scan/v0/unclaimed-development-fund-coupons: - name: 'unclaimed-development-fund-coupons' - type: 'unlimited' - /api/scan/v0/updates: - name: 'v0-updates' - type: 'unlimited' - /api/scan/v0/validators: - name: 'validators' - type: 'unlimited' - /api/scan/v0/voterequest: - name: 'voterequest' - type: 'unlimited' - /api/scan/v0/voterequests: - name: 'voterequests' - type: 'unlimited' - /api/scan/v1/domains: - name: 'domains-v1' - type: 'unlimited' - /api/scan/v1/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v1/state: - name: 'state' - type: 'unlimited' - /api/scan/v1/updates: - name: 'v1-updates' - type: 'banned' - /api/scan/v2/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v2/state: - name: 'state' - type: 'unlimited' - /api/scan/v2/updates: - name: 'v2-updates' - type: 'unlimited' - /api/scan/version: - name: 'version' - type: 'unlimited' /registry/allocation-instruction/v1/allocation-factory: fillInterval: '60s' maxTokens: 720 @@ -500,6 +355,58 @@ sv: tokensPerFill: 120 tokensPerFill: 720 type: 'limited' + synchronizer: + sequencer: + externalRateLimits: + globalLimits: + fillInterval: '60s' + maxTokens: 200000 + tokensPerFill: 200000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 10000 + tokensPerFill: 10000 + rateLimits: + /com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-authentication' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-connect' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit: + fillInterval: '60s' + maxTokens: 600 + name: 'sequencer-download-topology-state-for-init' + perIpLimits: + fillInterval: '60s' + maxTokens: 20 + tokensPerFill: 20 + tokensPerFill: 600 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-subscribe' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' svs: sv: cometbft: @@ -1872,9 +1779,10 @@ synchronizerMigration: pulumiStacksDir: 'cluster/stacks/prod' repoUrl: 'https://github.com/canton-network/splice' spliceRoot: 'splice' + sequencer: + enableBftSequencer: true version: 'local' frozenMigrationId: 0 - splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/deployment/scratchnetd/config.yaml b/cluster/deployment/scratchnetd/config.yaml index f6984aad726..9b7dbc3f65f 100644 --- a/cluster/deployment/scratchnetd/config.yaml +++ b/cluster/deployment/scratchnetd/config.yaml @@ -1 +1,10 @@ -!include($SPLICE_ROOT/cluster/configs/shared/scratchnet.yaml) {} +!include($SPLICE_ROOT/cluster/configs/shared/scratchnet.yaml) +infra: + gkeGateway: + # required for Cloud Armor to be attached to anything + proxyForIstioHttp: true +cloudArmor: + enabled: true + allRulesPreviewOnly: false + logging: + enabled: true diff --git a/cluster/deployment/scratchnete/config.resolved.yaml b/cluster/deployment/scratchnete/config.resolved.yaml index aa126001187..557c166e83e 100644 --- a/cluster/deployment/scratchnete/config.resolved.yaml +++ b/cluster/deployment/scratchnete/config.resolved.yaml @@ -4,15 +4,23 @@ cloudArmor: enabled: false publicEndpoints: publicScan: - hostname: 'scan.sv-2.scratcha.global.canton.network.digitalasset.com' + hostPrefixRegex: 'scan' pathPrefix: '/api/scan' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 0 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 2000 + withinIntervalSeconds: 60 + sequencer: + hostPrefixRegex: 'sequencer-[0-9]+' + pathPrefix: '/' + restrictToRateLimitedPaths: false + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 20000 withinIntervalSeconds: 60 tokenRegistry: + hostPrefixRegex: 'scan' pathPrefix: '/registry' - throttleAcrossAllEndpointsAllIps: - maxRequestsBeforeHttp429: 200 + throttleAcrossAllEndpointsPerIp: + maxRequestsBeforeHttp429: 1000 withinIntervalSeconds: 60 cluster: nodePools: @@ -26,14 +34,27 @@ cluster: minNodes: 1 nodeType: 'n4d-standard-8' infra: + enableSweetSecurity: true gkeGateway: proxyForIstioHttp: false istio: enableIngressAccessLogging: true enablePublicTokenRegistry: true - sequencerFlowControl: - initialConnectionWindowSize: 52428800 - initialStreamWindowSize: 524288 + flowControl: + internal: + initialConnectionWindowSize: 104857600 + initialStreamWindowSize: 1048576 + ports: + - 5001 + - 5002 + - 5007 + - 5009 + public: + initialConnectionWindowSize: 52428800 + initialStreamWindowSize: 524288 + ports: + - 5008 + - 5010 prometheus: retentionDuration: '30d' retentionSize: '200GB' @@ -205,181 +226,13 @@ sv: externalRateLimits: globalLimits: fillInterval: '60s' - maxTokens: 2147483647 - tokensPerFill: 2147483647 + maxTokens: 10000 + tokensPerFill: 10000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 1000 + tokensPerFill: 1000 rateLimits: - /api/scan/livez: - name: 'livez' - type: 'unlimited' - /api/scan/readyz: - name: 'readyz' - type: 'unlimited' - /api/scan/status: - name: 'status' - type: 'unlimited' - /api/scan/v0/acs: - fillInterval: '60s' - maxTokens: 500 - name: 'acs' - perIpLimits: - fillInterval: '60s' - maxTokens: 10 - tokensPerFill: 5 - tokensPerFill: 500 - type: 'limited' - /api/scan/v0/active-synchronizer-serial: - name: 'activeSynchronizerSerial' - type: 'unlimited' - /api/scan/v0/admin/sv/previous-sv-reward-weight: - name: 'sv-previous-sv-reward-weight' - type: 'unlimited' - /api/scan/v0/admin/sv/voterequests: - name: 'sv-voterequests' - type: 'unlimited' - /api/scan/v0/admin/sv/voteresults: - name: 'sv-voteresults' - type: 'unlimited' - /api/scan/v0/admin/validator: - name: 'validator-licenses' - type: 'unlimited' - /api/scan/v0/amulet-config-for-round: - name: 'amulet-config-for-round' - type: 'banned' - /api/scan/v0/amulet-price: - name: 'amulet-price-votes' - type: 'unlimited' - /api/scan/v0/amulet-rules: - name: 'amulet-rules' - type: 'unlimited' - /api/scan/v0/ans-entries: - name: 'ans-entries' - type: 'unlimited' - /api/scan/v0/ans-rules: - name: 'ans-rules' - type: 'unlimited' - /api/scan/v0/backfilling: - name: 'backfilling' - type: 'unlimited' - /api/scan/v0/closed-rounds: - name: 'closed-rounds' - type: 'unlimited' - /api/scan/v0/domains: - name: 'domains' - type: 'unlimited' - /api/scan/v0/dso: - name: 'dso-info' - type: 'unlimited' - /api/scan/v0/dso-party-id: - name: 'dso-party-id' - type: 'unlimited' - /api/scan/v0/dso-sequencers: - name: 'dso-sequencers' - type: 'unlimited' - /api/scan/v0/events: - name: 'events' - type: 'unlimited' - /api/scan/v0/external-party-amulet-rules: - name: 'external-party-amulet-rules' - type: 'unlimited' - /api/scan/v0/feature-support: - name: 'feature-support' - type: 'unlimited' - /api/scan/v0/featured-apps: - name: 'featured-apps' - type: 'unlimited' - /api/scan/v0/history/bulk/acs: - name: 'listBulkAcsSnapshotObjects' - type: 'unlimited' - /api/scan/v0/history/bulk/checksums: - name: 'getBulkHistoryChecksums' - type: 'unlimited' - /api/scan/v0/history/bulk/updates: - name: 'listBulkUpdateHistoryObjects' - type: 'unlimited' - /api/scan/v0/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v0/internal/reward-accounting-process: - name: 'reward-accounting-process' - type: 'unlimited' - /api/scan/v0/lsu: - name: 'lsu' - type: 'unlimited' - /api/scan/v0/migrations: - name: 'migrations-schedule' - type: 'unlimited' - /api/scan/v0/open-and-issuing-mining-rounds: - name: 'open-and-issuing-mining-rounds' - type: 'unlimited' - /api/scan/v0/roll-forward-lsu: - name: 'rollForwardLsu' - type: 'unlimited' - /api/scan/v0/scans: - name: 'scans' - type: 'unlimited' - /api/scan/v0/splice-instance-names: - name: 'splice-instance-names' - type: 'unlimited' - /api/scan/v0/state: - name: 'state' - type: 'unlimited' - /api/scan/v0/sv-bft-sequencers: - name: 'bft-sequencers' - type: 'unlimited' - /api/scan/v0/synchronizer-bootstrapping-transactions: - name: 'synchronizer-bootstrapping-transactions' - type: 'unlimited' - /api/scan/v0/synchronizer-identities: - name: 'synchronizer-identities' - type: 'unlimited' - /api/scan/v0/transfer-command: - name: 'transfer-command-status' - type: 'unlimited' - /api/scan/v0/transfer-command-counter: - name: 'transfer-command-counter' - type: 'unlimited' - /api/scan/v0/transfer-preapprovals: - name: 'transfer-preapprovals' - type: 'unlimited' - /api/scan/v0/unclaimed-development-fund-coupons: - name: 'unclaimed-development-fund-coupons' - type: 'unlimited' - /api/scan/v0/updates: - name: 'v0-updates' - type: 'unlimited' - /api/scan/v0/validators: - name: 'validators' - type: 'unlimited' - /api/scan/v0/voterequest: - name: 'voterequest' - type: 'unlimited' - /api/scan/v0/voterequests: - name: 'voterequests' - type: 'unlimited' - /api/scan/v1/domains: - name: 'domains-v1' - type: 'unlimited' - /api/scan/v1/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v1/state: - name: 'state' - type: 'unlimited' - /api/scan/v1/updates: - name: 'v1-updates' - type: 'banned' - /api/scan/v2/holdings: - name: 'holdings' - type: 'unlimited' - /api/scan/v2/state: - name: 'state' - type: 'unlimited' - /api/scan/v2/updates: - name: 'v2-updates' - type: 'unlimited' - /api/scan/version: - name: 'version' - type: 'unlimited' /registry/allocation-instruction/v1/allocation-factory: fillInterval: '60s' maxTokens: 720 @@ -500,6 +353,58 @@ sv: tokensPerFill: 120 tokensPerFill: 720 type: 'limited' + synchronizer: + sequencer: + externalRateLimits: + globalLimits: + fillInterval: '60s' + maxTokens: 200000 + tokensPerFill: 200000 + globalPerIpLimits: + fillInterval: '60s' + maxTokens: 10000 + tokensPerFill: 10000 + rateLimits: + /com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-authentication' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-connect' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit: + fillInterval: '60s' + maxTokens: 600 + name: 'sequencer-download-topology-state-for-init' + perIpLimits: + fillInterval: '60s' + maxTokens: 20 + tokensPerFill: 20 + tokensPerFill: 600 + type: 'limited' + /com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe: + fillInterval: '60s' + maxTokens: 10000 + name: 'sequencer-subscribe' + perIpLimits: + fillInterval: '60s' + maxTokens: 100 + tokensPerFill: 100 + tokensPerFill: 10000 + type: 'limited' svs: sv: cometbft: @@ -1872,9 +1777,10 @@ synchronizerMigration: pulumiStacksDir: 'cluster/stacks/prod' repoUrl: 'https://github.com/canton-network/splice' spliceRoot: 'splice' + sequencer: + enableBftSequencer: true version: 'local' frozenMigrationId: 0 - splitSvDeploymentEnabled: true validator1: deduplicationDuration: '30m' logging: diff --git a/cluster/expected/gcp/expected.json b/cluster/expected/gcp/expected.json index 73250cb1ae9..a45b9fbf615 100644 --- a/cluster/expected/gcp/expected.json +++ b/cluster/expected/gcp/expected.json @@ -154,6 +154,17 @@ "provider": "", "type": "cn:gcp:ServiceAccount" }, + { + "custom": true, + "id": "", + "inputs": { + "cloudArmorTier": "CA_STANDARD", + "project": "da-cn-example-project-id" + }, + "name": "cloud-armor-tier", + "provider": "", + "type": "gcp:compute/projectCloudArmorTier:ProjectCloudArmorTier" + }, { "custom": true, "id": "", diff --git a/cluster/expected/infra/expected.json b/cluster/expected/infra/expected.json index 00f93211212..6342d143839 100644 --- a/cluster/expected/infra/expected.json +++ b/cluster/expected/infra/expected.json @@ -1940,7 +1940,7 @@ "meshConfig": { "accessLogEncoding": "JSON", "accessLogFile": "", - "accessLogFormat": "{\"trace_id\":\"%REQ(traceparent)%\",\"authority\":\"%REQ(:AUTHORITY)%\",\"bytes_received\":\"%BYTES_RECEIVED%\",\"bytes_sent\":\"%BYTES_SENT%\",\"downstream_local_address\":\"%DOWNSTREAM_LOCAL_ADDRESS%\",\"downstream_remote_address\":\"%DOWNSTREAM_REMOTE_ADDRESS%\",\"duration\":\"%DURATION%\",\"method\":\"%REQ(:METHOD)%\",\"path\":\"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%\",\"protocol\":\"%PROTOCOL%\",\"request_id\":\"%REQ(X-REQUEST-ID)%\",\"requested_server_name\":\"%REQUESTED_SERVER_NAME%\",\"response_code\":\"%RESPONSE_CODE%\",\"response_code_details\":\"%RESPONSE_CODE_DETAILS%\",\"response_flags\":\"%RESPONSE_FLAGS%\",\"start_time\":\"%START_TIME%\",\"upstream_cluster\":\"%UPSTREAM_CLUSTER%\",\"upstream_host\":\"%UPSTREAM_HOST%\",\"upstream_local_address\":\"%UPSTREAM_LOCAL_ADDRESS%\",\"upstream_service_time\":\"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%\",\"user_agent\":\"%REQ(USER-AGENT)%\",\"x_forwarded_for\":\"%REQ(X-FORWARDED-FOR)%\",\"local_rate_limited\":\"%RESP(x-local-rate-limit)%\",\"rate_limit_limit\":\"%RESP(x-ratelimit-limit)%\",\"rate_limit_remaining\":\"%RESP(x-ratelimit-remaining)%\",\"rate_limit_reset\":\"%RESP(x-ratelimit-reset)%\"}", + "accessLogFormat": "{\"trace_id\":\"%REQ(traceparent)%\",\"authority\":\"%REQ(:AUTHORITY)%\",\"bytes_received\":\"%BYTES_RECEIVED%\",\"bytes_sent\":\"%BYTES_SENT%\",\"downstream_local_address\":\"%DOWNSTREAM_LOCAL_ADDRESS%\",\"downstream_remote_address\":\"%DOWNSTREAM_REMOTE_ADDRESS%\",\"duration\":\"%DURATION%\",\"method\":\"%REQ(:METHOD)%\",\"path\":\"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%\",\"protocol\":\"%PROTOCOL%\",\"request_id\":\"%REQ(X-REQUEST-ID)%\",\"requested_server_name\":\"%REQUESTED_SERVER_NAME%\",\"response_code\":\"%RESPONSE_CODE%\",\"response_code_details\":\"%RESPONSE_CODE_DETAILS%\",\"grpc_status\":\"%GRPC_STATUS(CAMEL_STRING)%\",\"response_flags\":\"%RESPONSE_FLAGS%\",\"start_time\":\"%START_TIME%\",\"upstream_cluster\":\"%UPSTREAM_CLUSTER%\",\"upstream_host\":\"%UPSTREAM_HOST%\",\"upstream_local_address\":\"%UPSTREAM_LOCAL_ADDRESS%\",\"upstream_service_time\":\"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%\",\"user_agent\":\"%REQ(USER-AGENT)%\",\"x_forwarded_for\":\"%REQ(X-FORWARDED-FOR)%\",\"envoy_external_address\":\"%REQ(X-ENVOY-EXTERNAL-ADDRESS)%\",\"masked_remote_address\":\"%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%\",\"local_rate_limited\":\"%RESP(x-local-rate-limit)%\",\"rate_limit_limit\":\"%RESP(x-ratelimit-limit)%\",\"rate_limit_remaining\":\"%RESP(x-ratelimit-remaining)%\",\"rate_limit_reset\":\"%RESP(x-ratelimit-reset)%\"}", "defaultConfig": { "gatewayTopology": { "numTrustedProxies": 0 @@ -2304,7 +2304,7 @@ "apiVersion": "networking.istio.io/v1alpha3", "kind": "EnvoyFilter", "metadata": { - "name": "sequencer-flow-control", + "name": "flow-control", "namespace": "cluster-ingress" }, "spec": { @@ -2331,8 +2331,8 @@ "interval": "30s", "timeout": "5s" }, - "initial_connection_window_size": 52428800, - "initial_stream_window_size": 524288 + "initial_connection_window_size": 104857600, + "initial_stream_window_size": 1048576 } } } @@ -2395,6 +2395,122 @@ } } } + }, + { + "applyTo": "CLUSTER", + "match": { + "cluster": { + "portNumber": 5001 + } + }, + "patch": { + "operation": "MERGE", + "value": { + "typed_extension_protocol_options": { + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": { + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions", + "use_downstream_protocol_config": { + "http2_protocol_options": { + "connection_keepalive": { + "interval": "30s", + "timeout": "5s" + }, + "initial_connection_window_size": 104857600, + "initial_stream_window_size": 1048576 + }, + "http_protocol_options": {} + } + } + } + } + } + }, + { + "applyTo": "CLUSTER", + "match": { + "cluster": { + "portNumber": 5002 + } + }, + "patch": { + "operation": "MERGE", + "value": { + "typed_extension_protocol_options": { + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": { + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions", + "use_downstream_protocol_config": { + "http2_protocol_options": { + "connection_keepalive": { + "interval": "30s", + "timeout": "5s" + }, + "initial_connection_window_size": 104857600, + "initial_stream_window_size": 1048576 + }, + "http_protocol_options": {} + } + } + } + } + } + }, + { + "applyTo": "CLUSTER", + "match": { + "cluster": { + "portNumber": 5007 + } + }, + "patch": { + "operation": "MERGE", + "value": { + "typed_extension_protocol_options": { + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": { + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions", + "use_downstream_protocol_config": { + "http2_protocol_options": { + "connection_keepalive": { + "interval": "30s", + "timeout": "5s" + }, + "initial_connection_window_size": 104857600, + "initial_stream_window_size": 1048576 + }, + "http_protocol_options": {} + } + } + } + } + } + }, + { + "applyTo": "CLUSTER", + "match": { + "cluster": { + "portNumber": 5009 + } + }, + "patch": { + "operation": "MERGE", + "value": { + "typed_extension_protocol_options": { + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": { + "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions", + "use_downstream_protocol_config": { + "http2_protocol_options": { + "connection_keepalive": { + "interval": "30s", + "timeout": "5s" + }, + "initial_connection_window_size": 104857600, + "initial_stream_window_size": 1048576 + }, + "http_protocol_options": {} + } + } + } + } + } } ] } diff --git a/cluster/expected/observability/expected.json b/cluster/expected/observability/expected.json index 4e1fd4ab672..8cb164ca815 100644 --- a/cluster/expected/observability/expected.json +++ b/cluster/expected/observability/expected.json @@ -84,7 +84,7 @@ "dso_missed_confirmations_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: dso party missed confirmations\n folder: canton-network\n interval: 1m\n rules:\n - uid: ddso1miss0conf0a\n title: DSO Party Missed Confirmations\n condition: missed confirmations threshold exceeded\n data:\n - refId: missed_confirmations\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: max by (party) (increase(daml_mediator_timeout_non_responsive_participants_total{party=~\"DSO::.*\"}[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: missed_confirmations\n - refId: missed confirmations threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: missed_confirmations\n intervalMs: 1000\n maxDataPoints: 43200\n refId: missed confirmations threshold exceeded\n type: threshold\n dashboardUid: cnck4jp\n panelId: 1\n noDataState: OK\n execErrState: Alerting\n for: 0m\n annotations:\n __dashboardUid__: cnck4jp\n __panelId__: \"1\"\n severity: critical\n summary: The DSO party missed {{ index $values \"missed_confirmations\" }}, more than 0 of its confirmations over the last 10 minutes.\n labels: {}\n isPaused: false\n", "extra_k8s_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: storage\n folder: k8s\n interval: 5m\n rules:\n - uid: adlrbu5kog0sga\n title: KubePersistentVolumeTooFull\n condition: free_space_below_threshold\n data:\n - refId: free_space\n relativeTimeRange:\n from: 360\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: kubelet_volume_stats_available_bytes{job=\"kubelet\",metrics_path=\"/metrics\",namespace=~\".*\"} / kubelet_volume_stats_capacity_bytes{job=\"kubelet\",metrics_path=\"/metrics\",namespace=~\".*\"}\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: free_space\n - refId: free_space_below_threshold\n relativeTimeRange:\n from: 360\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.15\n - 0\n type: lt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: free_space\n intervalMs: 1000\n maxDataPoints: 43200\n refId: free_space_below_threshold\n type: threshold\n dashboardUid: 919b92a8e8041bd567af9edab12c840c\n panelId: 2\n noDataState: Alerting\n execErrState: Alerting\n for: 5m\n annotations:\n __dashboardUid__: 919b92a8e8041bd567af9edab12c840c\n __panelId__: \"2\"\n severity: warning\n description: The PersistentVolume claimed by {{ index $labels \"persistentvolumeclaim\" }} in Namespace {{ index $labels \"namespace\" }} is running out of disk space. Currently {{ humanizePercentage (index $values \"free_space\").Value }} is available.\n runbook_url: https://runbooks.prometheus-operator.dev/runbooks/kubernetes/kubepersistentvolumefillingup\n summary: PersistentVolume is too full.\n isPaused: false\n", "global-sync-health_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: global-synchronizer-health\n folder: canton-network\n interval: 1m\n rules:\n # Confirmation requests that were sequenced but never processed by the mediator\n # (e.g. dropped due to CometBFT replays). There is no dedicated Canton metric for\n # discarded events yet, so we approximate it as the difference between sequenced\n # send-confirmation-request events and requests received by the mediator.\n - uid: gsh0discarded0cr\n title: High Rate of Discarded Confirmation Requests\n condition: discarded fraction threshold exceeded\n data:\n - refId: discarded_fraction\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: (sum by (namespace) (rate(daml_sequencer_block_events_total{type=\"send-confirmation-request\"}[10m])) - sum by (namespace) (rate(daml_mediator_requests_total[10m]))) / sum by (namespace) (rate(daml_sequencer_block_events_total{type=\"send-confirmation-request\"}[10m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: discarded_fraction\n - refId: discarded fraction threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.2\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: discarded_fraction\n intervalMs: 1000\n maxDataPoints: 43200\n refId: discarded fraction threshold exceeded\n type: threshold\n dashboardUid: fe8wt04z620aof\n panelId: 10\n noDataState: OK\n execErrState: OK\n for: 10m\n annotations:\n __dashboardUid__: fe8wt04z620aof\n __panelId__: \"10\"\n runbook_url: \"\"\n severity: warning\n summary: \"{{ $labels.namespace }} discarded more than 0.2 of sequenced confirmation requests over the last 10 minutes\"\n description: 'A significant fraction of sequenced confirmation requests was never processed by the mediator. This typically matches CometBFT replays.'\n labels: {}\n isPaused: false\n # Failure rate of confirmation requests, as observed by the mediator.\n # Can increase for non-network reasons, but is relatively unlikely to.\n - uid: gsh0failedcr0rel\n title: High Failed Confirmation Request Rate\n condition: failure rate threshold exceeded\n data:\n - refId: failure_rate\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: 1 - sum by (namespace) (rate(daml_mediator_approved_requests_total[30m])) / sum by (namespace) (rate(daml_mediator_requests_total{duplicate_reject=\"false\"}[30m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: failure_rate\n - refId: failure rate threshold exceeded\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n type: gt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: failure_rate\n intervalMs: 1000\n maxDataPoints: 43200\n refId: failure rate threshold exceeded\n type: threshold\n dashboardUid: fe8wt04z620aof\n panelId: 8\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: fe8wt04z620aof\n __panelId__: \"8\"\n runbook_url: \"\"\n severity: warning\n summary: \"{{ $labels.namespace }} saw more than 0.1 of confirmation requests fail over the last 30 minutes\"\n description: 'Confirmation requests are failing at a high rate. Can happen for non-network reasons, but is relatively unlikely to.'\n labels: {}\n isPaused: false\n # Overall TPS (approved confirmation requests per second), compared to the\n # previous 30m window. Can drop for non-network reasons, but a large relative\n # drop is a strong signal for network issues.\n - uid: gsh0tps0drop0rel\n title: Significant TPS Drop\n condition: tps dropped\n data:\n - refId: A\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by (namespace) (rate(daml_mediator_approved_requests_total[30m]))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: B\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n editorMode: code\n expr: sum by (namespace) (rate(daml_mediator_approved_requests_total[30m] offset 30m))\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n - refId: C\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: $A < $B * 0.5\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: tps dropped\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: C\n intervalMs: 1000\n maxDataPoints: 43200\n refId: tps dropped\n type: threshold\n dashboardUid: fe8wt04z620aof\n panelId: 4\n noDataState: OK\n execErrState: OK\n for: 5m\n annotations:\n __dashboardUid__: fe8wt04z620aof\n __panelId__: \"4\"\n runbook_url: \"\"\n severity: warning\n summary: \"{{ $labels.namespace }} saw TPS drop below 0.5 of the previous 30 minute window\"\n description: 'The rate of approved confirmation requests over the last 30m dropped significantly compared to the previous 30m window. Can happen for non-network reasons, but is relatively unlikely to.'\n labels: {}\n isPaused: false\n", - "istio-rate-limiting_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: istio-rate-limit\n folder: platform\n interval: 5m\n rules:\n - uid: cfurlpifrmvi8w\n title: Enforced requests\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 21600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\"sv.*\"}[$__rate_interval])) by (namespace, pod)\n instant: true\n interval: \"\"\n intervalMs: 30000\n legendFormat: '{{namespace}} - {{pod}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnr56dj\n panelId: 9\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n runbook_url: \"\"\n severity: warning\n description: Envoy local rate limiting is active, requests are being rejected. Check the \"Istio Rate Limiting\" dashboard.\n summary: Envoy is rate limiting requests in {{ $labels.namespace }} on pod {{ $labels.pod }}.\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429\n isPaused: false\n", + "istio-rate-limiting_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: istio-rate-limit\n folder: platform\n interval: 5m\n rules:\n - uid: cfurlpifrmvi8w\n title: Enforced requests\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 21600\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n editorMode: code\n expr: sum(rate(envoy_http_local_rate_limit_enforced{namespace=~\"sv.*\"}[$__rate_interval])) by (namespace, pod, limiter)\n instant: true\n interval: \"\"\n intervalMs: 30000\n legendFormat: '{{namespace}} - {{pod}} - {{limiter}}'\n maxDataPoints: 43200\n range: false\n refId: A\n - refId: C\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: A\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n dashboardUid: cnr56dj\n panelId: 9\n noDataState: OK\n execErrState: Alerting\n for: 5m\n annotations:\n runbook_url: \"\"\n severity: warning\n description: Envoy local rate limiting is active, requests are being rejected. The limiter label tells which limit rejected them (global, per_ip, endpoint_per_ip); the sidecar access log of a rejected request reports the same limit in the local_rate_limited field. Check the \"Istio Rate Limiting\" dashboard.\n summary: Envoy is rate limiting requests in {{ $labels.namespace }} on pod {{ $labels.pod }} ({{ $labels.limiter }} limit).\n labels:\n \"\": \"\"\n gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0A%28jsonPayload.response_code=429%20OR%20jsonPayload.grpc_status=%22ResourceExhausted%22%29\n isPaused: false\n", "load-tester_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: load-tester\n folder: canton-network\n interval: 1m\n rules:\n - uid: a1dbc4e8-7941-4351-9a14-f8573fd2be2b\n title: K6 Request Failure Rate Exceeded\n condition: D\n data:\n - refId: A\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: sum(k6_http_reqs_total)\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: B\n relativeTimeRange:\n from: 300\n to: 0\n datasourceUid: prometheus\n model:\n datasource:\n type: prometheus\n uid: prometheus\n disableTextWrap: false\n editorMode: builder\n expr: sum(k6_http_reqs_total{expected_response=\"false\"}) or on() vector(0)\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: B\n useBackend: false\n - refId: C\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: ($B / $A) * 100\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: math\n - refId: D\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 50\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: C\n intervalMs: 1000\n maxDataPoints: 43200\n refId: D\n type: threshold\n noDataState: Alerting\n execErrState: Alerting\n for: 1m\n annotations:\n description: ''\n runbook_url: ''\n summary: The k6 load tester experienced HTTP request failures at a rate past the acceptable threshold\n labels:\n gcloud_filter: ''\n isPaused: false\n - uid: ddogbytusmz9cb\n title: K6 Throughput Below Threshold\n condition: C\n data:\n - refId: A\n relativeTimeRange:\n from: 3600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: rate(k6_transfers_completed_total{scenario=\"generate_load\"}[$__rate_interval])\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: A\n useBackend: false\n - refId: C\n relativeTimeRange:\n from: 3600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.123\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: B\n intervalMs: 1000\n maxDataPoints: 43200\n refId: C\n type: threshold\n - refId: B\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0\n - 0\n type: gt\n operator:\n type: and\n query:\n params: []\n reducer:\n params: []\n type: avg\n type: query\n datasource:\n name: Expression\n type: __expr__\n uid: __expr__\n expression: A\n hide: false\n reducer: mean\n refId: B\n type: reduce\n dashboardUid: ccbb2351-2ae2-462f-ae0e-f2c893ad1028\n panelId: 28\n noDataState: Alerting\n execErrState: Alerting\n for: 15m # give some time for restarts\n annotations:\n __dashboardUid__: ccbb2351-2ae2-462f-ae0e-f2c893ad1028\n __panelId__: \"28\"\n severity: warning\n description: ''\n runbook_url: ''\n summary: Transaction rate from the k6 load tester is lower than expected threshold\n labels:\n '': ''\n isPaused: false\n", "mining-rounds_alerts.yaml": "apiVersion: 1\ngroups:\n - orgId: 1\n name: mining-rounds\n folder: canton-network\n interval: 1m\n rules:\n - uid: ae74w21qca2o0e\n title: Open Mining Rounds Not Advancing\n condition: No new round\n data:\n - refId: Round number diff\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: max(max(max_over_time(splice_sv_dso_store_latest_open_mining_round[30m])) by (namespace) - min(min_over_time(splice_sv_dso_store_latest_open_mining_round[30m])) by (namespace))\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: Round number diff\n useBackend: false\n - refId: No new round\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.9\n type: gt\n datasource:\n type: __expr__\n uid: __expr__\n expression: Round number diff\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No new round\n type: threshold\n dashboardUid: ed94a332-4fa7-47f8-982b-fc997381175b\n panelId: 1\n noDataState: Alerting\n execErrState: OK\n for: 1m\n annotations:\n __dashboardUid__: ed94a332-4fa7-47f8-982b-fc997381175b\n __panelId__: \"1\"\n runbook_url: \"\"\n summary: \"The open mining rounds have not advanced in the last 30m\"\n description: 'None of our SV apps have seen the open mining round advancing in the last 30m. Either all of our SVs or a high enough number of SVs to break BFT guarantees are failing'\n severity: critical\n labels:\n priority: high\n isPaused: false\n - uid: ae74w21qca2o0d\n title: Issuing Rounds Not Advancing\n condition: No new round\n data:\n - refId: Round number diff\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: prometheus\n model:\n disableTextWrap: false\n editorMode: builder\n expr: max(max(max_over_time(splice_sv_dso_store_latest_issuing_mining_round[30m])) by (namespace) - min(min_over_time(splice_sv_dso_store_latest_issuing_mining_round[30m])) by (namespace))\n fullMetaSearch: false\n includeNullMetadata: true\n instant: true\n intervalMs: 1000\n legendFormat: __auto\n maxDataPoints: 43200\n range: false\n refId: Round number diff\n useBackend: false\n - refId: No new round\n relativeTimeRange:\n from: 600\n to: 0\n datasourceUid: __expr__\n model:\n conditions:\n - evaluator:\n params:\n - 0.1\n type: lt\n operator:\n type: and\n query:\n params:\n - C\n reducer:\n params: []\n type: last\n type: query\n unloadEvaluator:\n params:\n - 0.9\n type: gt\n datasource:\n type: __expr__\n uid: __expr__\n expression: Round number diff\n intervalMs: 1000\n maxDataPoints: 43200\n refId: No new round\n type: threshold\n dashboardUid: ed94a332-4fa7-47f8-982b-fc997381175b\n panelId: 1\n noDataState: Alerting\n execErrState: OK\n for: 1m\n annotations:\n __dashboardUid__: ed94a332-4fa7-47f8-982b-fc997381175b\n __panelId__: \"2\"\n runbook_url: \"\"\n summary: \"The issuing mining rounds have not advanced in the last 30m\"\n description: 'None of our SV apps have seen the issuing mining round advancing in the last 30m. Either all of our SVs or a high enough number of SVs to break BFT guarantees are failing'\n severity: critical\n labels:\n priority: high\n isPaused: false\n", "mute_time_intervals.yaml": "apiVersion: 1\nmuteTimes:\n - orgId: 1\n name: sv1-mute\n time_intervals:\n - times:\n - start_time: '18:00'\n end_time: '24:00'\n weekdays:\n - wednesday\n location: UTC\n - orgId: 1\n name: sv-runbook-mute\n time_intervals:\n - times:\n - start_time: '18:00'\n end_time: '24:00'\n weekdays:\n - wednesday\n location: UTC\n - times:\n - start_time: '00:00'\n end_time: '06:00'\n - start_time: '12:00'\n end_time: '13:00'\n weekdays:\n - thursday\n location: UTC\n", @@ -327,7 +327,7 @@ "health.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 2,\n \"id\": 9,\n \"links\": [],\n \"liveNow\": false,\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"fillOpacity\": 71,\n \"lineWidth\": 0\n },\n \"mappings\": [\n {\n \"options\": {\n \"0\": {\n \"index\": 1,\n \"text\": \"inactive\"\n },\n \"1\": {\n \"index\": 0,\n \"text\": \"active\"\n }\n },\n \"type\": \"value\"\n }\n ],\n \"max\": 1,\n \"min\": 0,\n \"noValue\": \"0\",\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"dark-red\",\n \"value\": null\n },\n {\n \"color\": \"green\",\n \"value\": 1\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"colWidth\": 1,\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": false\n },\n \"rowHeight\": 0.8,\n \"showValue\": \"never\",\n \"tooltip\": {\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"9.3.2\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"daml_health_status{namespace=\\\"$namespace\\\", pod=~\\\"$pod\\\"}\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"legendFormat\": \"{{pod}} {{component}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Components' Status\",\n \"type\": \"status-history\"\n }\n ],\n \"refresh\": false,\n \"schemaVersion\": 37,\n \"style\": \"dark\",\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"Prometheus\",\n \"value\": \"Prometheus\"\n },\n \"hide\": 2,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"datasource\",\n \"options\": [],\n \"query\": \"prometheus\",\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"type\": \"datasource\"\n },\n {\n \"current\": {\n \"selected\": false,\n \"text\": \"static_local\",\n \"value\": \"static_local\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_health_status, namespace)\",\n \"hide\": 0,\n \"includeAll\": false,\n \"multi\": false,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_health_status, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 2,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"selected\": true,\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"${datasource}\"\n },\n \"definition\": \"label_values(daml_health_status{namespace=\\\"$namespace\\\"}, pod)\",\n \"hide\": 0,\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"pod\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(daml_health_status{namespace=\\\"$namespace\\\"}, pod)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"skipUrlSync\": false,\n \"sort\": 0,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-3h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Health\",\n \"uid\": \"IME_mP2Vk\",\n \"version\": 1,\n \"weekStart\": \"\"\n}\n", "http.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"id\": 1182,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 15,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"label_replace(histogram_count(rate(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\", status_code=~\\\"2.*\\\"}[$__rate_interval])), \\\"source_display\\\", \\\"Source UI: $1\\\", \\\"source_ui\\\", \\\"(.+)\\\")\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}} {{source_display}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Handled Requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 12,\n \"w\": 9,\n \"x\": 15,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 10,\n \"w\": 24,\n \"x\": 0,\n \"y\": 12\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, rate(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Request Timing quantile $percentile\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"requests\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 22\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.1.1\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"daml_http_requests_inflight{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\", operation=~\\\"$operation\\\"}\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": true,\n \"legendFormat\": \"{{namespace}} {{job}} {{http_service}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"In Flight Requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 41,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_service)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"service\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\"},operation)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"operation\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_service=~\\\"$service\\\"},operation)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [\n {\n \"selected\": true,\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n {\n \"selected\": false,\n \"text\": \"0.95\",\n \"value\": \"0.95\"\n },\n {\n \"selected\": false,\n \"text\": \"0.99\",\n \"value\": \"0.99\"\n }\n ],\n \"query\": \"0.9,0.95,0.99\",\n \"type\": \"custom\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-5m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Http Server\",\n \"uid\": \"aerbb811lwveod\",\n \"version\": 4\n}\n", "http_client.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 1,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"disableTextWrap\": false,\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code=~\\\"2.*\\\"}[$__rate_interval]))\",\n \"fullMetaSearch\": false,\n \"includeNullMetadata\": false,\n \"legendFormat\": \"{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\",\n \"useBackend\": false\n }\n ],\n \"title\": \"Client requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"s\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 9\n },\n \"id\": 4,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_quantile($percentile, rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\", operation=~\\\"$operation\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Client Requests Timing quantile 0.95 \",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 15,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"histogram_count(rate(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"{{namespace}} {{job}} {{http_client}} {{target_host}} {{operation}} {{status}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"thresholds\"\n },\n \"custom\": {\n \"align\": \"auto\",\n \"cellOptions\": {\n \"type\": \"auto\"\n },\n \"footer\": {\n \"reducers\": []\n },\n \"inspect\": false\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": [\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"status_code\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 125\n }\n ]\n },\n {\n \"matcher\": {\n \"id\": \"byName\",\n \"options\": \"target_host\"\n },\n \"properties\": [\n {\n \"id\": \"custom.width\",\n \"value\": 375\n }\n ]\n }\n ]\n },\n \"gridPos\": {\n \"h\": 11,\n \"w\": 9,\n \"x\": 15,\n \"y\": 17\n },\n \"id\": 5,\n \"options\": {\n \"cellHeight\": \"sm\",\n \"showHeader\": true,\n \"sortBy\": [\n {\n \"desc\": true,\n \"displayName\": \"Value\"\n }\n ]\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"builder\",\n \"exemplar\": false,\n \"expr\": \"sum by(namespace, job, http_client, operation, status_code, target_host) (histogram_count(increase(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", operation=~\\\"$operation\\\", status_code!~\\\"2.*\\\"}[$__range])))\",\n \"format\": \"table\",\n \"instant\": true,\n \"legendFormat\": \"__auto\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Errors\",\n \"transformations\": [\n {\n \"id\": \"organize\",\n \"options\": {\n \"excludeByName\": {\n \"Time\": true,\n \"endpoint\": true,\n \"instance\": true\n },\n \"includeByName\": {},\n \"indexByName\": {\n \"Time\": 2,\n \"Value\": 15,\n \"endpoint\": 3,\n \"http_client\": 4,\n \"instance\": 5,\n \"job\": 1,\n \"namespace\": 0,\n \"node_name\": 6,\n \"node_type\": 7,\n \"operation\": 8,\n \"otel_scope_name\": 9,\n \"pod\": 10,\n \"service\": 11,\n \"status\": 12,\n \"status_code\": 13,\n \"target_host\": 14\n },\n \"renameByName\": {}\n }\n }\n ],\n \"type\": \"table\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 28\n },\n \"id\": 3,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": true,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"daml_http_client_requests_inflight{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\", operation=~\\\"$operation\\\"}\\n\",\n \"legendFormat\": \"{{namespace}} {{job}} {{target_host}} {{http_client}} {{operation}} {{status_code}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"In Flight Client Requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"validator1\"\n ],\n \"value\": [\n \"validator1\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds,namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds,namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"job\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\"},job)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"All\"\n ],\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\"},operation)\",\n \"description\": \"\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"operation\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\", http_client=~\\\"$http_client\\\"},operation)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"definition\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_client)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"http_client\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(daml_http_client_requests_duration_seconds{namespace=~\\\"$namespace\\\", job=~\\\"$job\\\"},http_client)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"current\": {\n \"text\": \"0.9\",\n \"value\": \"0.9\"\n },\n \"name\": \"percentile\",\n \"options\": [],\n \"query\": \"0.9,0.95,0.99\",\n \"type\": \"custom\",\n \"valuesFormat\": \"csv\"\n },\n {\n \"baseFilters\": [],\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"filters\": [],\n \"name\": \"filter\",\n \"type\": \"adhoc\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-15m\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Http Client\",\n \"uid\": \"a8a3113a-bccf-4728-b752-dd7a5d6f9bda\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", - "istio.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of HTTP 429 (Too Many Requests) responses per second, broken down by the namespace reporting the request and the destination service receiving it.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, destination_service) (rate(istio_requests_total{response_code=\\\"429\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Istio total rejected requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Fraction of requests evaluated by the Envoy local rate limit filter that were rejected with HTTP 429, per namespace and pod.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percent\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\\n/\\nsum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejection ratio\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of number of requests for which rate limiting was applied (e.g.: 429 returned)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Enforced requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for responses without an available token (but not necessarily enforced)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 8\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_rate_limited{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace,job)\",\n \"legendFormat\": \"{{namespace}} - {{job}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejected requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for number of requests for which the rate limiter was consulted\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 16\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Enabled requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for number of requests under limit responses from the token bucket\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 16\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_ok{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod)\",\n \"legendFormat\": \"{{namespace}} - {{pod}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Allowed requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Istio Rate Limiting\",\n \"uid\": \"cnr56dj\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", + "istio.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of rejected requests per second, broken down by the namespace reporting the request and the destination service receiving it. Rejections are HTTP 429 (Too Many Requests) responses, or, for gRPC calls (e.g. the sequencer API), RESOURCE_EXHAUSTED (grpc_response_status 8) responses, which envoy returns with HTTP 200.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n }\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum by(namespace, destination_service) (rate(istio_requests_total{response_code=\\\"429\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval])) + sum by(namespace, destination_service) (rate(istio_requests_total{grpc_response_status=\\\"8\\\", namespace=~\\\"$namespace\\\"}[$__rate_interval]))\",\n \"legendFormat\": \"__auto\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Istio total rejected requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Fraction of the Envoy local rate limit evaluations that were rejected (HTTP 429, or gRPC RESOURCE_EXHAUSTED for gRPC APIs such as the sequencer), per namespace, pod and limiter. One filter per limit is installed (limiter=global, per_ip, endpoint_per_ip) and every request is evaluated by each of them.\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"percent\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 1,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"exemplar\": false,\n \"expr\": \"sum(rate(envoy_http_local_rate_limit_enforced{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod, limiter)\\n/\\nsum(rate(envoy_http_local_rate_limit_enabled{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod, limiter)\",\n \"format\": \"time_series\",\n \"instant\": false,\n \"interval\": \"\",\n \"legendFormat\": \"{{namespace}} - {{pod}} - {{limiter}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejection ratio\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate of number of requests for which rate limiting was applied (HTTP 429, or gRPC RESOURCE_EXHAUSTED for gRPC APIs such as the sequencer), broken down by the limit that rejected them: global, per_ip (per client IP) and endpoint_per_ip (per endpoint and client IP).\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 9,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limit_enforced{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod, limiter)\",\n \"legendFormat\": \"{{namespace}} - {{pod}} - {{limiter}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Enforced requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for responses without an available token (but not necessarily enforced)\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 8\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limit_rate_limited{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, job, limiter)\",\n \"legendFormat\": \"{{namespace}} - {{job}} - {{limiter}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejected requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for number of requests for which the rate limiter was consulted\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 16\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limit_enabled{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod, limiter)\",\n \"legendFormat\": \"{{namespace}} - {{pod}} - {{limiter}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Enabled requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Rate for number of requests under limit responses from the token bucket\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 16\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"single\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(envoy_http_local_rate_limit_ok{namespace=~\\\"$namespace\\\"}[$__rate_interval])) by (namespace, pod, limiter)\",\n \"legendFormat\": \"{{namespace}} - {{pod}} - {{limiter}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Allowed requests\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"schemaVersion\": 42,\n \"tags\": [],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(namespace)\",\n \"includeAll\": true,\n \"multi\": true,\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(namespace)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-6h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Istio Rate Limiting\",\n \"uid\": \"cnr56dj\",\n \"version\": 3,\n \"weekStart\": \"\"\n}\n", "rate_limiters.json": "{\n \"annotations\": {\n \"list\": [\n {\n \"builtIn\": 1,\n \"datasource\": {\n \"type\": \"grafana\",\n \"uid\": \"-- Grafana --\"\n },\n \"enable\": true,\n \"hide\": true,\n \"iconColor\": \"rgba(0, 211, 255, 1)\",\n \"name\": \"Annotations & Alerts\",\n \"target\": {\n \"limit\": 100,\n \"matchAny\": false,\n \"tags\": [],\n \"type\": \"dashboard\"\n },\n \"type\": \"dashboard\"\n }\n ]\n },\n \"editable\": true,\n \"fiscalYearStartMonth\": 0,\n \"graphTooltip\": 0,\n \"links\": [],\n \"panels\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 0,\n \"y\": 0\n },\n \"id\": 2,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\", result!=\\\"accepted\\\"}[$__rate_interval])) by (result)\",\n \"instant\": false,\n \"legendFormat\": \"{{ result }}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Rejections\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n }\n },\n \"mappings\": [],\n \"unit\": \"short\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 12,\n \"x\": 12,\n \"y\": 0\n },\n \"id\": 4,\n \"options\": {\n \"displayLabels\": [\n \"name\",\n \"percent\"\n ],\n \"legend\": {\n \"displayMode\": \"list\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"values\": [\n \"percent\",\n \"value\"\n ]\n },\n \"pieType\": \"pie\",\n \"reduceOptions\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"fields\": \"\",\n \"values\": false\n },\n \"sort\": \"desc\",\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"none\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(increase(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__range])) by (result)\",\n \"instant\": true,\n \"legendFormat\": \"{{result}}\",\n \"range\": false,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Requests by Result\",\n \"type\": \"piechart\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 8\n },\n \"id\": 5,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__rate_interval])) by (limiter, limiter_type, limiter_attribute)\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}} {{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Total Requests\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 17\n },\n \"id\": 6,\n \"options\": {\n \"legend\": {\n \"calcs\": [],\n \"displayMode\": \"list\",\n \"placement\": \"bottom\",\n \"showLegend\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\", result!=\\\"accepted\\\"}[$__rate_interval])) by (result, limiter, limiter_type, limiter_attribute)\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}} {{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Rejections\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 8,\n \"w\": 24,\n \"x\": 0,\n \"y\": 25\n },\n \"id\": 7,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"splice_rate_limiting_max_limit_per_second{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\"}\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}}{{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Limits\",\n \"type\": \"timeseries\"\n },\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"description\": \"Cannot extract the attribute value\",\n \"fieldConfig\": {\n \"defaults\": {\n \"color\": {\n \"mode\": \"palette-classic\"\n },\n \"custom\": {\n \"axisBorderShow\": false,\n \"axisCenteredZero\": false,\n \"axisColorMode\": \"text\",\n \"axisLabel\": \"\",\n \"axisPlacement\": \"auto\",\n \"barAlignment\": 0,\n \"barWidthFactor\": 0.6,\n \"drawStyle\": \"line\",\n \"fillOpacity\": 0,\n \"gradientMode\": \"none\",\n \"hideFrom\": {\n \"legend\": false,\n \"tooltip\": false,\n \"viz\": false\n },\n \"insertNulls\": false,\n \"lineInterpolation\": \"linear\",\n \"lineWidth\": 1,\n \"pointSize\": 5,\n \"scaleDistribution\": {\n \"type\": \"linear\"\n },\n \"showPoints\": \"auto\",\n \"showValues\": false,\n \"spanNulls\": false,\n \"stacking\": {\n \"group\": \"A\",\n \"mode\": \"none\"\n },\n \"thresholdsStyle\": {\n \"mode\": \"off\"\n }\n },\n \"mappings\": [],\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": 0\n },\n {\n \"color\": \"red\",\n \"value\": 80\n }\n ]\n },\n \"unit\": \"reqps\"\n },\n \"overrides\": []\n },\n \"gridPos\": {\n \"h\": 9,\n \"w\": 24,\n \"x\": 0,\n \"y\": 33\n },\n \"id\": 8,\n \"options\": {\n \"legend\": {\n \"calcs\": [\n \"lastNotNull\"\n ],\n \"displayMode\": \"table\",\n \"placement\": \"right\",\n \"showLegend\": true,\n \"sortBy\": \"Last *\",\n \"sortDesc\": true\n },\n \"tooltip\": {\n \"hideZeros\": false,\n \"mode\": \"multi\",\n \"sort\": \"desc\"\n }\n },\n \"pluginVersion\": \"12.4.0\",\n \"targets\": [\n {\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"editorMode\": \"code\",\n \"expr\": \"sum(rate(rate_limiting_unknown_attribute_not_limited{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", limiter=~\\\"$limiter\\\", node_name=~\\\"$node_name\\\"}[$__rate_interval])) by (limiter, limiter_type, limiter_attribute)\",\n \"instant\": false,\n \"legendFormat\": \"{{ limiter }} {{limiter_type}} {{limiter_attribute}}\",\n \"range\": true,\n \"refId\": \"A\"\n }\n ],\n \"title\": \"Attribute Limits not enforced\",\n \"type\": \"timeseries\"\n }\n ],\n \"preload\": false,\n \"refresh\": \"1m\",\n \"schemaVersion\": 42,\n \"tags\": [\n \"prometheus\",\n \"rate-limiting\"\n ],\n \"templating\": {\n \"list\": [\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"sv-1\",\n \"value\": \"sv-1\"\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total, namespace)\",\n \"includeAll\": false,\n \"label\": \"Namespace\",\n \"name\": \"namespace\",\n \"options\": [],\n \"query\": {\n \"query\": \"label_values(splice_rate_limiting_total, namespace)\",\n \"refId\": \"StandardVariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": [\n \"scan-app\"\n ],\n \"value\": [\n \"scan-app\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\"},node_name)\",\n \"includeAll\": true,\n \"label\": \"Node Name\",\n \"multi\": true,\n \"name\": \"node_name\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\"},node_name)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 3,\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\"},http_service)\",\n \"includeAll\": true,\n \"label\": \"HTTP Service\",\n \"multi\": true,\n \"name\": \"http_service\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", node_name=~\\\"$node_name\\\"},http_service)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 1,\n \"type\": \"query\"\n },\n {\n \"allowCustomValue\": false,\n \"current\": {\n \"text\": \"All\",\n \"value\": [\n \"$__all\"\n ]\n },\n \"datasource\": {\n \"type\": \"prometheus\",\n \"uid\": \"prometheus\"\n },\n \"definition\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", node_name=~\\\"$node_name\\\"},limiter)\",\n \"includeAll\": true,\n \"label\": \"Limiter\",\n \"multi\": true,\n \"name\": \"limiter\",\n \"options\": [],\n \"query\": {\n \"qryType\": 1,\n \"query\": \"label_values(splice_rate_limiting_total{namespace=~\\\"$namespace\\\", http_service=~\\\"$http_service\\\", node_name=~\\\"$node_name\\\"},limiter)\",\n \"refId\": \"PrometheusVariableQueryEditor-VariableQuery\"\n },\n \"refresh\": 1,\n \"regex\": \"\",\n \"regexApplyTo\": \"value\",\n \"sort\": 2,\n \"type\": \"query\"\n }\n ]\n },\n \"time\": {\n \"from\": \"now-12h\",\n \"to\": \"now\"\n },\n \"timepicker\": {},\n \"timezone\": \"\",\n \"title\": \"Splice Rate Limiting\",\n \"uid\": \"splice-rate-limit-db\",\n \"version\": 2,\n \"weekStart\": \"\"\n}\n" }, "kind": "ConfigMap", @@ -591,6 +591,24 @@ "__name__" ] }, + { + "action": "replace", + "regex": "envoy_http_local_rate_limiter_(.+)_http_local_rate_limit_.+", + "replacement": "$1", + "sourceLabels": [ + "__name__" + ], + "targetLabel": "limiter" + }, + { + "action": "replace", + "regex": "envoy_http_local_rate_limiter_.+_http_local_rate_limit_(.+)", + "replacement": "envoy_http_local_rate_limit_$1", + "sourceLabels": [ + "__name__" + ], + "targetLabel": "__name__" + }, { "action": "labeldrop", "regex": "instance" diff --git a/cluster/expected/sv-canton/expected.json b/cluster/expected/sv-canton/expected.json index 116be57591f..96f5fa56e43 100644 --- a/cluster/expected/sv-canton/expected.json +++ b/cluster/expected/sv-canton/expected.json @@ -1527,6 +1527,882 @@ "provider": "", "type": "kubernetes:core/v1:Secret" }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-10-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-1-global-domain-10-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-7-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-1-global-domain-7-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-8-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-1-global-domain-8-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-9-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-1-global-domain-9-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-10-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-da-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-da-1-global-domain-10-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-7-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-da-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-da-1-global-domain-7-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-8-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-da-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-da-1-global-domain-8-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-9-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv-da-1", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-da-1-global-domain-9-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-10-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-global-domain-10-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-7-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-global-domain-7-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-8-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-global-domain-8-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, + { + "custom": false, + "id": "", + "inputs": { + "appLabel": "global-domain-9-sequencer", + "globalLimits": { + "fillInterval": "60s", + "maxTokens": 200000, + "tokensPerFill": 200000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "inboundPort": 5008, + "namespace": "sv", + "protocol": "grpc", + "rateLimits": { + "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-authentication", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-connect", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit": { + "fillInterval": "60s", + "maxTokens": 600, + "name": "sequencer-download-topology-state-for-init", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 20, + "tokensPerFill": 20 + }, + "tokensPerFill": 600, + "type": "limited" + }, + "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe": { + "fillInterval": "60s", + "maxTokens": 10000, + "name": "sequencer-subscribe", + "perIpLimits": { + "fillInterval": "60s", + "maxTokens": 100, + "tokensPerFill": 100 + }, + "tokensPerFill": 10000, + "type": "limited" + } + } + }, + "name": "splice-sv-global-domain-9-sequencer-rate-limit", + "provider": "", + "type": "splice:RateLimit" + }, { "custom": true, "id": "", @@ -1906,62 +2782,730 @@ "type": "gcp:sql/database:Database" }, { - "custom": false, + "custom": true, "id": "", - "inputs": {}, - "name": "sv-1-global-domain-10", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-10-sequencer-rate-limit-access-log", + "namespace": "sv-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-10-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-10-sequencer-rate-limit-access-log", "provider": "", - "type": "canton:network:domain:global" + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", - "compat": "true", - "maxHistory": 10, - "name": "global-domain-10", - "namespace": "sv-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-10-sequencer-rate-limit", + "namespace": "sv-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" } - ] + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } } - ] + } } - } - }, - "apiRequestLogLevel": "DEBUG", - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "enableAntiAffinity": true, - "enablePostgresMetrics": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "logAsyncFlush": false, - "logLevel": "INFO", - "logLevelStdout": "DEBUG", - "mediator": { - "additionalEnvVars": [ + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-10-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-10-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "sv-1-global-domain-10", + "provider": "", + "type": "canton:network:domain:global" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", + "compat": "true", + "maxHistory": 10, + "name": "global-domain-10", + "namespace": "sv-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "enableAntiAffinity": true, + "enablePostgresMetrics": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "logAsyncFlush": false, + "logLevel": "INFO", + "logLevelStdout": "DEBUG", + "mediator": { + "additionalEnvVars": [ { "name": "CUSTOM_MOCK_ENV_VAR_NAME", "value": "CUSTOM_MOCK_ENV_VAR_VALUE" @@ -2042,6 +3586,674 @@ "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-7-sequencer-rate-limit-access-log", + "namespace": "sv-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-7-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-7-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-7-sequencer-rate-limit", + "namespace": "sv-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-7-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-7-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": false, "id": "", @@ -2176,26 +4388,694 @@ "type": "kubernetes:helm.sh/v3:Release" }, { - "custom": false, + "custom": true, "id": "", - "inputs": {}, - "name": "sv-1-global-domain-8", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-8-sequencer-rate-limit-access-log", + "namespace": "sv-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-8-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-8-sequencer-rate-limit-access-log", "provider": "", - "type": "canton:network:domain:global" + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", - "compat": "true", - "maxHistory": 10, - "name": "global-domain-8", - "namespace": "sv-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-8-sequencer-rate-limit", + "namespace": "sv-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-8-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-8-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "sv-1-global-domain-8", + "provider": "", + "type": "canton:network:domain:global" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", + "compat": "true", + "maxHistory": 10, + "name": "global-domain-8", + "namespace": "sv-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { "requiredDuringSchedulingIgnoredDuringExecution": { "nodeSelectorTerms": [ { @@ -2308,6 +5188,674 @@ "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-9-sequencer-rate-limit-access-log", + "namespace": "sv-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-9-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-9-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-9-sequencer-rate-limit", + "namespace": "sv-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-9-sequencer" + } + } + } + }, + "name": "sv-1-global-domain-9-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": false, "id": "", @@ -5530,21 +9078,1494 @@ "type": "gcp:sql/database:Database" }, { - "custom": false, + "custom": true, "id": "", - "inputs": {}, - "name": "sv-da-1-global-domain-10", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-10-sequencer-rate-limit-access-log", + "namespace": "sv-da-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-10-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-10-sequencer-rate-limit-access-log", "provider": "", - "type": "canton:network:domain:global" + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", - "compat": "true", - "maxHistory": 10, - "name": "global-domain-10", + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-10-sequencer-rate-limit", + "namespace": "sv-da-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-10-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-10-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "sv-da-1-global-domain-10", + "provider": "", + "type": "canton:network:domain:global" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", + "compat": "true", + "maxHistory": 10, + "name": "global-domain-10", + "namespace": "sv-da-1", + "timeout": 600, + "values": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "cn_apps", + "operator": "Exists" + }, + { + "key": "cn_apps", + "operator": "In", + "values": [ + "hyperdisk" + ] + } + ] + } + ] + } + } + }, + "apiRequestLogLevel": "DEBUG", + "cluster": { + "dnsName": "mock.global.canton.network.digitalasset.com", + "fixedTokens": false, + "hostname": "mock.global.canton.network.digitalasset.com", + "name": "cn-mocknet" + }, + "enableAntiAffinity": true, + "enablePostgresMetrics": true, + "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", + "logAsyncFlush": false, + "logLevel": "INFO", + "logLevelStdout": "DEBUG", + "mediator": { + "additionalEnvVars": [ + { + "name": "CUSTOM_MOCK_ENV_VAR_NAME", + "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", + "persistence": { + "databaseName": "global_domain_10_mediator", + "port": 5432, + "postgresName": "mediator-10-pg", + "secretName": "mediator-10-pg-secrets" + }, + "resources": { + "limits": { + "memory": "2Gi" + }, + "requests": { + "cpu": "1", + "memory": "1Gi" + } + } + }, + "metrics": { + "enable": true, + "migration": { + "id": 10 + } + }, + "sequencer": { + "additionalEnvVars": [ + { + "name": "ADDITIONAL_CONFIG_SEQUENCER_RATE_LIMITS", + "value": "canton.sequencers.sequencer.sequencer.block.throughput-cap.messages.confirmation-request {\n global-tps-cap = 2\n per-client-tps-cap = 1\n global-kbps-cap = 200\n per-client-kbps-cap = 100\n}\ncanton.sequencers.sequencer.sequencer.block.throughput-cap.messages.topology {\n global-tps-cap = 4\n per-client-tps-cap = 3\n global-kbps-cap = 400\n per-client-kbps-cap = 300\n}" + }, + { + "name": "CUSTOM_MOCK_ENV_VAR_NAME", + "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + } + ], + "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", + "driver": { + "externalAddress": "sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com", + "externalPort": 443, + "persistence": { + "databaseName": "sequencer_10_dabft", + "secretName": "sequencer-bft-10-pg-secrets" + }, + "type": "cantonbft" + }, + "persistence": { + "databaseName": "global_domain_10_sequencer", + "port": 5432, + "postgresName": "sequencer-10-pg", + "secretName": "sequencer-10-pg-secrets" + }, + "resources": { + "limits": { + "memory": "2Gi" + }, + "requests": { + "cpu": "1", + "memory": "1Gi" + } + } + }, + "serviceAccountName": "sv-canton-migration-10", + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.21" + }, + "name": "sv-da-1-global-domain-10", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-7-sequencer-rate-limit-access-log", + "namespace": "sv-da-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-7-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-7-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-7-sequencer-rate-limit", + "namespace": "sv-da-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-7-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-7-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, + { + "custom": false, + "id": "", + "inputs": {}, + "name": "sv-da-1-global-domain-7", + "provider": "", + "type": "canton:network:domain:global" + }, + { + "custom": true, + "id": "", + "inputs": { + "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", + "compat": "true", + "maxHistory": 10, + "name": "global-domain-7", "namespace": "sv-da-1", "timeout": 600, "values": { @@ -5593,10 +10614,10 @@ ], "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { - "databaseName": "global_domain_10_mediator", + "databaseName": "global_domain_7_mediator", "port": 5432, - "postgresName": "mediator-10-pg", - "secretName": "mediator-10-pg-secrets" + "postgresName": "mediator-7-pg", + "secretName": "mediator-7-pg-secrets" }, "resources": { "limits": { @@ -5611,7 +10632,7 @@ "metrics": { "enable": true, "migration": { - "id": 10 + "id": 7 } }, "sequencer": { @@ -5627,19 +10648,15 @@ ], "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { - "externalAddress": "sequencer-p2p-10.sv-1.mock.global.canton.network.digitalasset.com", - "externalPort": 443, - "persistence": { - "databaseName": "sequencer_10_dabft", - "secretName": "sequencer-bft-10-pg-secrets" - }, - "type": "cantonbft" + "host": "global-domain-7-cometbft-cometbft-rpc.sv-da-1.svc.cluster.local", + "port": 26657, + "type": "cometbft" }, "persistence": { - "databaseName": "global_domain_10_sequencer", + "databaseName": "global_domain_7_sequencer", "port": 5432, - "postgresName": "sequencer-10-pg", - "secretName": "sequencer-10-pg-secrets" + "postgresName": "sequencer-7-pg", + "secretName": "sequencer-7-pg-secrets" }, "resources": { "limits": { @@ -5651,7 +10668,7 @@ } } }, - "serviceAccountName": "sv-canton-migration-10", + "serviceAccountName": "sv-canton-migration-7", "tolerations": [ { "effect": "NoSchedule", @@ -5660,17 +10677,685 @@ } ] }, - "version": "0.3.21" + "version": "0.3.20" }, - "name": "sv-da-1-global-domain-10", + "name": "sv-da-1-global-domain-7", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-8-sequencer-rate-limit-access-log", + "namespace": "sv-da-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-8-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-8-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-8-sequencer-rate-limit", + "namespace": "sv-da-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-8-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-8-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": false, "id": "", "inputs": {}, - "name": "sv-da-1-global-domain-7", + "name": "sv-da-1-global-domain-8", "provider": "", "type": "canton:network:domain:global" }, @@ -5681,7 +11366,7 @@ "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", "compat": "true", "maxHistory": 10, - "name": "global-domain-7", + "name": "global-domain-8", "namespace": "sv-da-1", "timeout": 600, "values": { @@ -5730,10 +11415,10 @@ ], "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", "persistence": { - "databaseName": "global_domain_7_mediator", + "databaseName": "global_domain_8_mediator", "port": 5432, - "postgresName": "mediator-7-pg", - "secretName": "mediator-7-pg-secrets" + "postgresName": "mediator-8-pg", + "secretName": "mediator-8-pg-secrets" }, "resources": { "limits": { @@ -5748,7 +11433,7 @@ "metrics": { "enable": true, "migration": { - "id": 7 + "id": 8 } }, "sequencer": { @@ -5764,15 +11449,15 @@ ], "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", "driver": { - "host": "global-domain-7-cometbft-cometbft-rpc.sv-da-1.svc.cluster.local", + "host": "global-domain-8-cometbft-cometbft-rpc.sv-da-1.svc.cluster.local", "port": 26657, "type": "cometbft" }, "persistence": { - "databaseName": "global_domain_7_sequencer", + "databaseName": "global_domain_8_sequencer", "port": 5432, - "postgresName": "sequencer-7-pg", - "secretName": "sequencer-7-pg-secrets" + "postgresName": "sequencer-8-pg", + "secretName": "sequencer-8-pg-secrets" }, "resources": { "limits": { @@ -5784,7 +11469,7 @@ } } }, - "serviceAccountName": "sv-canton-migration-7", + "serviceAccountName": "sv-canton-migration-8", "tolerations": [ { "effect": "NoSchedule", @@ -5795,142 +11480,677 @@ }, "version": "0.3.20" }, - "name": "sv-da-1-global-domain-7", + "name": "sv-da-1-global-domain-8", "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, { - "custom": false, + "custom": true, "id": "", - "inputs": {}, - "name": "sv-da-1-global-domain-8", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-9-sequencer-rate-limit-access-log", + "namespace": "sv-da-1" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-9-sequencer" + } + } + } + }, + "name": "sv-da-1-global-domain-9-sequencer-rate-limit-access-log", "provider": "", - "type": "canton:network:domain:global" + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" }, { "custom": true, "id": "", "inputs": { - "chart": "oci://ghcr.io/digital-asset/decentralized-canton-sync-dev/helm/splice-global-domain", - "compat": "true", - "maxHistory": 10, - "name": "global-domain-8", - "namespace": "sv-da-1", - "timeout": 600, - "values": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "cn_apps", - "operator": "Exists" - }, - { - "key": "cn_apps", - "operator": "In", - "values": [ - "hyperdisk" - ] + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-9-sequencer-rate-limit", + "namespace": "sv-da-1" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" } - ] + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } } - ] - } - } - }, - "apiRequestLogLevel": "DEBUG", - "cluster": { - "dnsName": "mock.global.canton.network.digitalasset.com", - "fixedTokens": false, - "hostname": "mock.global.canton.network.digitalasset.com", - "name": "cn-mocknet" - }, - "enableAntiAffinity": true, - "enablePostgresMetrics": true, - "imageRepo": "us-central1-docker.pkg.dev/da-cn-shared/ghcr/digital-asset/decentralized-canton-sync-dev/docker", - "logAsyncFlush": false, - "logLevel": "INFO", - "logLevelStdout": "DEBUG", - "mediator": { - "additionalEnvVars": [ - { - "name": "CUSTOM_MOCK_ENV_VAR_NAME", - "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + } } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomMediatorJvmFlag", - "persistence": { - "databaseName": "global_domain_8_mediator", - "port": 5432, - "postgresName": "mediator-8-pg", - "secretName": "mediator-8-pg-secrets" }, - "resources": { - "limits": { - "memory": "2Gi" + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } }, - "requests": { - "cpu": "1", - "memory": "1Gi" + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } } - } - }, - "metrics": { - "enable": true, - "migration": { - "id": 8 - } - }, - "sequencer": { - "additionalEnvVars": [ - { - "name": "ADDITIONAL_CONFIG_SEQUENCER_RATE_LIMITS", - "value": "canton.sequencers.sequencer.sequencer.block.throughput-cap.messages.confirmation-request {\n global-tps-cap = 2\n per-client-tps-cap = 1\n global-kbps-cap = 200\n per-client-kbps-cap = 100\n}\ncanton.sequencers.sequencer.sequencer.block.throughput-cap.messages.topology {\n global-tps-cap = 4\n per-client-tps-cap = 3\n global-kbps-cap = 400\n per-client-kbps-cap = 300\n}" + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } }, - { - "name": "CUSTOM_MOCK_ENV_VAR_NAME", - "value": "CUSTOM_MOCK_ENV_VAR_VALUE" + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } } - ], - "additionalJvmOptions": "-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -DcustomSequencerJvmFlag", - "driver": { - "host": "global-domain-8-cometbft-cometbft-rpc.sv-da-1.svc.cluster.local", - "port": 26657, - "type": "cometbft" }, - "persistence": { - "databaseName": "global_domain_8_sequencer", - "port": 5432, - "postgresName": "sequencer-8-pg", - "secretName": "sequencer-8-pg-secrets" + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } }, - "resources": { - "limits": { - "memory": "2Gi" + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } }, - "requests": { - "cpu": "1", - "memory": "1Gi" + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } } } - }, - "serviceAccountName": "sv-canton-migration-8", - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" + ], + "workloadSelector": { + "labels": { + "app": "global-domain-9-sequencer" } - ] - }, - "version": "0.3.20" + } + } }, - "name": "sv-da-1-global-domain-8", + "name": "sv-da-1-global-domain-9-sequencer-rate-limit", "provider": "", - "type": "kubernetes:helm.sh/v3:Release" + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" }, { "custom": false, @@ -8411,6 +14631,674 @@ "provider": "", "type": "gcp:sql/database:Database" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-10-sequencer-rate-limit-access-log", + "namespace": "sv" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-10-sequencer" + } + } + } + }, + "name": "sv-global-domain-10-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-10-sequencer-rate-limit", + "namespace": "sv" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-10-sequencer" + } + } + } + }, + "name": "sv-global-domain-10-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": false, "id": "", @@ -8532,21 +15420,689 @@ "memory": "1Gi" } } - }, - "serviceAccountName": "sv-canton-migration-10", - "tolerations": [ - { - "effect": "NoSchedule", - "key": "cn_apps", - "operator": "Exists" + }, + "serviceAccountName": "sv-canton-migration-10", + "tolerations": [ + { + "effect": "NoSchedule", + "key": "cn_apps", + "operator": "Exists" + } + ] + }, + "version": "0.3.21" + }, + "name": "sv-global-domain-10", + "provider": "", + "type": "kubernetes:helm.sh/v3:Release" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-7-sequencer-rate-limit-access-log", + "namespace": "sv" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-7-sequencer" + } + } + } + }, + "name": "sv-global-domain-7-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-7-sequencer-rate-limit", + "namespace": "sv" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-7-sequencer" } - ] - }, - "version": "0.3.21" + } + } }, - "name": "sv-global-domain-10", + "name": "sv-global-domain-7-sequencer-rate-limit", "provider": "", - "type": "kubernetes:helm.sh/v3:Release" + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" }, { "custom": false, @@ -8681,6 +16237,674 @@ "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-8-sequencer-rate-limit-access-log", + "namespace": "sv" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-8-sequencer" + } + } + } + }, + "name": "sv-global-domain-8-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-8-sequencer-rate-limit", + "namespace": "sv" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-8-sequencer" + } + } + } + }, + "name": "sv-global-domain-8-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": false, "id": "", @@ -8814,6 +17038,674 @@ "provider": "", "type": "kubernetes:helm.sh/v3:Release" }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "telemetry.istio.io/v1", + "kind": "Telemetry", + "metadata": { + "name": "global-domain-9-sequencer-rate-limit-access-log", + "namespace": "sv" + }, + "spec": { + "accessLogging": [ + { + "filter": { + "expression": "response.grpc_status == 8" + }, + "providers": [ + { + "name": "envoy" + } + ] + } + ], + "selector": { + "matchLabels": { + "app": "global-domain-9-sequencer" + } + } + } + }, + "name": "sv-global-domain-9-sequencer-rate-limit-access-log", + "provider": "", + "type": "kubernetes:telemetry.istio.io/v1:Telemetry" + }, + { + "custom": true, + "id": "", + "inputs": { + "apiVersion": "networking.istio.io/v1alpha3", + "kind": "EnvoyFilter", + "metadata": { + "name": "global-domain-9-sequencer-rate-limit", + "namespace": "sv" + }, + "spec": { + "configPatches": [ + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5008 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_global" + } + } + } + } + }, + { + "applyTo": "HTTP_ROUTE", + "match": { + "context": "SIDECAR_INBOUND", + "routeConfiguration": { + "vhost": { + "name": "inbound|http|5008", + "route": { + "action": "ANY" + } + } + } + }, + "patch": { + "operation": "MERGE", + "value": { + "route": { + "rate_limits": [ + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-connect", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerConnectService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-authentication", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerAuthenticationService/" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-subscribe", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + } + ] + }, + { + "actions": [ + { + "header_value_match": { + "descriptor_value": "sequencer-download-topology-state-for-init", + "expect_match": true, + "headers": [ + { + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/com.digitalasset.canton.sequencer.api.v30.SequencerService/DownloadTopologyStateForInit" + } + } + ] + } + }, + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] + } + ] + }, + "typed_per_filter_config": { + "envoy.filters.http.local_ratelimit": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 200000, + "tokens_per_fill": 200000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 600, + "tokens_per_fill": 600 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-connect" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-authentication" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-subscribe" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 100, + "tokens_per_fill": 100 + } + }, + { + "entries": [ + { + "key": "header_match", + "value": "sequencer-download-topology-state-for-init" + }, + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 20, + "tokens_per_fill": 20 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": true, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "global-domain-9-sequencer" + } + } + } + }, + "name": "sv-global-domain-9-sequencer-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, { "custom": false, "id": "", diff --git a/cluster/expected/sv-runbook/expected.json b/cluster/expected/sv-runbook/expected.json index 201dda3b888..65eb7244b17 100644 --- a/cluster/expected/sv-runbook/expected.json +++ b/cluster/expected/sv-runbook/expected.json @@ -827,240 +827,18 @@ "appLabel": "scan-app", "globalLimits": { "fillInterval": "60s", - "maxTokens": 2147483647, - "tokensPerFill": 2147483647 + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 1000, + "tokensPerFill": 1000 }, "inboundPort": 5012, "namespace": "sv", + "protocol": "http", "rateLimits": { - "/api/scan/livez": { - "name": "livez", - "type": "unlimited" - }, - "/api/scan/readyz": { - "name": "readyz", - "type": "unlimited" - }, - "/api/scan/status": { - "name": "status", - "type": "unlimited" - }, - "/api/scan/v0/acs": { - "fillInterval": "60s", - "maxTokens": 500, - "name": "acs", - "perIpLimits": { - "fillInterval": "60s", - "maxTokens": 10, - "tokensPerFill": 5 - }, - "tokensPerFill": 500, - "type": "limited" - }, - "/api/scan/v0/active-synchronizer-serial": { - "name": "activeSynchronizerSerial", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/previous-sv-reward-weight": { - "name": "sv-previous-sv-reward-weight", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voterequests": { - "name": "sv-voterequests", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voteresults": { - "name": "sv-voteresults", - "type": "unlimited" - }, - "/api/scan/v0/admin/validator": { - "name": "validator-licenses", - "type": "unlimited" - }, - "/api/scan/v0/amulet-config-for-round": { - "name": "amulet-config-for-round", - "type": "banned" - }, - "/api/scan/v0/amulet-price": { - "name": "amulet-price-votes", - "type": "unlimited" - }, - "/api/scan/v0/amulet-rules": { - "name": "amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/ans-entries": { - "name": "ans-entries", - "type": "unlimited" - }, - "/api/scan/v0/ans-rules": { - "name": "ans-rules", - "type": "unlimited" - }, - "/api/scan/v0/backfilling": { - "name": "backfilling", - "type": "unlimited" - }, - "/api/scan/v0/closed-rounds": { - "name": "closed-rounds", - "type": "unlimited" - }, - "/api/scan/v0/domains": { - "name": "domains", - "type": "unlimited" - }, - "/api/scan/v0/dso": { - "name": "dso-info", - "type": "unlimited" - }, - "/api/scan/v0/dso-party-id": { - "name": "dso-party-id", - "type": "unlimited" - }, - "/api/scan/v0/dso-sequencers": { - "name": "dso-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/events": { - "name": "events", - "type": "unlimited" - }, - "/api/scan/v0/external-party-amulet-rules": { - "name": "external-party-amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/feature-support": { - "name": "feature-support", - "type": "unlimited" - }, - "/api/scan/v0/featured-apps": { - "name": "featured-apps", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/acs": { - "name": "listBulkAcsSnapshotObjects", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/checksums": { - "name": "getBulkHistoryChecksums", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/updates": { - "name": "listBulkUpdateHistoryObjects", - "type": "unlimited" - }, - "/api/scan/v0/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v0/internal/reward-accounting-process": { - "name": "reward-accounting-process", - "type": "unlimited" - }, - "/api/scan/v0/lsu": { - "name": "lsu", - "type": "unlimited" - }, - "/api/scan/v0/migrations": { - "name": "migrations-schedule", - "type": "unlimited" - }, - "/api/scan/v0/open-and-issuing-mining-rounds": { - "name": "open-and-issuing-mining-rounds", - "type": "unlimited" - }, - "/api/scan/v0/roll-forward-lsu": { - "name": "rollForwardLsu", - "type": "unlimited" - }, - "/api/scan/v0/scans": { - "name": "scans", - "type": "unlimited" - }, - "/api/scan/v0/splice-instance-names": { - "name": "splice-instance-names", - "type": "unlimited" - }, - "/api/scan/v0/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v0/sv-bft-sequencers": { - "name": "bft-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-bootstrapping-transactions": { - "name": "synchronizer-bootstrapping-transactions", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-identities": { - "name": "synchronizer-identities", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command": { - "name": "transfer-command-status", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command-counter": { - "name": "transfer-command-counter", - "type": "unlimited" - }, - "/api/scan/v0/transfer-preapprovals": { - "name": "transfer-preapprovals", - "type": "unlimited" - }, - "/api/scan/v0/unclaimed-development-fund-coupons": { - "name": "unclaimed-development-fund-coupons", - "type": "unlimited" - }, - "/api/scan/v0/updates": { - "name": "v0-updates", - "type": "unlimited" - }, - "/api/scan/v0/validators": { - "name": "validators", - "type": "unlimited" - }, - "/api/scan/v0/voterequest": { - "name": "voterequest", - "type": "unlimited" - }, - "/api/scan/v0/voterequests": { - "name": "voterequests", - "type": "unlimited" - }, - "/api/scan/v1/domains": { - "name": "domains-v1", - "type": "unlimited" - }, - "/api/scan/v1/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v1/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v1/updates": { - "name": "v1-updates", - "type": "banned" - }, - "/api/scan/v2/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v2/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v2/updates": { - "name": "v2-updates", - "type": "unlimited" - }, - "/api/scan/version": { - "name": "version", - "type": "unlimited" - }, "/registry/allocation-instruction/v1/allocation-factory": { "fillInterval": "60s", "maxTokens": 720, @@ -1675,10 +1453,104 @@ "listener": { "filterChain": { "filter": { - "name": "envoy.filters.network.http_connection_manager" + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" } } } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } }, "patch": { "operation": "INSERT_BEFORE", @@ -1688,7 +1560,7 @@ "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", "value": { - "stat_prefix": "http_local_rate_limiter" + "stat_prefix": "http_local_rate_limiter_global" } } } @@ -1712,50 +1584,6 @@ "value": { "route": { "rate_limits": [ - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - } - ] - }, - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - }, - { - "masked_remote_address": { - "v4_prefix_mask_len": 32, - "v6_prefix_mask_len": 128 - } - } - ] - }, { "actions": [ { @@ -1945,6 +1773,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v1" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } } ] } @@ -1964,6 +1800,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v1" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } } ] } @@ -2165,6 +2009,14 @@ "ignore_case": true, "prefix": "/registry/allocation-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } } ] } @@ -2184,6 +2036,14 @@ "ignore_case": true, "prefix": "/registry/allocation-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } } ] } @@ -2253,6 +2113,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } } ] } @@ -2272,6 +2140,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } } ] } @@ -2283,47 +2159,92 @@ } } ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] } ] }, "typed_per_filter_config": { "envoy.filters.http.local_ratelimit": { "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, "descriptors": [ { "entries": [ { "key": "header_match", - "value": "acs" + "value": "registry-allocations" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 500, - "tokens_per_fill": 500 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "acs" - }, - { - "key": "masked_remote_address" + "value": "registry-metadata-info" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 10, - "tokens_per_fill": 5 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocations" + "value": "registry-metadata-instruments" } ], "token_bucket": { @@ -2336,23 +2257,20 @@ "entries": [ { "key": "header_match", - "value": "registry-allocations" - }, - { - "key": "masked_remote_address" + "value": "registry-allocation-factory" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-info" + "value": "registry-transfer-instruction" } ], "token_bucket": { @@ -2365,40 +2283,33 @@ "entries": [ { "key": "header_match", - "value": "registry-metadata-info" - }, - { - "key": "masked_remote_address", - "value": "192.68.78.50/32" + "value": "registry-transfer-factory" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 250, - "tokens_per_fill": 250 + "max_tokens": 1440, + "tokens_per_fill": 1440 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-info" - }, - { - "key": "masked_remote_address" + "value": "registry-settlement-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-instruments" + "value": "registry-allocations-v2" } ], "token_bucket": { @@ -2411,23 +2322,20 @@ "entries": [ { "key": "header_match", - "value": "registry-metadata-instruments" - }, - { - "key": "masked_remote_address" + "value": "registry-allocation-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-factory" + "value": "registry-allocation-instruction-v2" } ], "token_bucket": { @@ -2440,23 +2348,20 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-factory" - }, - { - "key": "masked_remote_address" + "value": "registry-transfer-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-transfer-instruction" + "value": "registry-transfer-instruction-v2" } ], "token_bucket": { @@ -2464,12 +2369,50 @@ "max_tokens": 720, "tokens_per_fill": 720 } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ { "entries": [ { "key": "header_match", - "value": "registry-transfer-instruction" + "value": "registry-allocations" }, { "key": "masked_remote_address" @@ -2485,20 +2428,24 @@ "entries": [ { "key": "header_match", - "value": "registry-transfer-factory" + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address", + "value": "192.68.78.50/32" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 1440, - "tokens_per_fill": 1440 + "max_tokens": 250, + "tokens_per_fill": 250 } }, { "entries": [ { "key": "header_match", - "value": "registry-transfer-factory" + "value": "registry-metadata-info" }, { "key": "masked_remote_address" @@ -2506,28 +2453,15 @@ ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 240, - "tokens_per_fill": 240 - } - }, - { - "entries": [ - { - "key": "header_match", - "value": "registry-settlement-factory-v2" - } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-settlement-factory-v2" + "value": "registry-metadata-instruments" }, { "key": "masked_remote_address" @@ -2543,20 +2477,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocations-v2" + "value": "registry-allocation-factory" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocations-v2" + "value": "registry-transfer-instruction" }, { "key": "masked_remote_address" @@ -2572,20 +2509,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-factory-v2" + "value": "registry-transfer-factory" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 240, + "tokens_per_fill": 240 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-factory-v2" + "value": "registry-settlement-factory-v2" }, { "key": "masked_remote_address" @@ -2601,20 +2541,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-instruction-v2" + "value": "registry-allocations-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-instruction-v2" + "value": "registry-allocation-factory-v2" }, { "key": "masked_remote_address" @@ -2630,13 +2573,16 @@ "entries": [ { "key": "header_match", - "value": "registry-transfer-factory-v2" + "value": "registry-allocation-instruction-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { @@ -2660,28 +2606,65 @@ { "key": "header_match", "value": "registry-transfer-instruction-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ { "entries": [ - { - "key": "header_match", - "value": "registry-transfer-instruction-v2" - }, { "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 1000, + "tokens_per_fill": 1000 } } ], @@ -2701,20 +2684,21 @@ "runtime_key": "local_rate_limit_enforced" }, "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, "response_headers_to_add": [ { "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "x-local-rate-limit", - "value": "true" + "value": "per_ip" } } ], - "stat_prefix": "http_local_rate_limiter", + "stat_prefix": "http_local_rate_limiter_per_ip", "token_bucket": { "fill_interval": "60s", - "max_tokens": 2147483647, - "tokens_per_fill": 2147483647 + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 } } } diff --git a/cluster/expected/sv/expected.json b/cluster/expected/sv/expected.json index c0157a8b85d..ab6d2a3927c 100644 --- a/cluster/expected/sv/expected.json +++ b/cluster/expected/sv/expected.json @@ -1522,240 +1522,18 @@ "appLabel": "scan-app", "globalLimits": { "fillInterval": "60s", - "maxTokens": 2147483647, - "tokensPerFill": 2147483647 + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 1000, + "tokensPerFill": 1000 }, "inboundPort": 5012, "namespace": "sv-1", + "protocol": "http", "rateLimits": { - "/api/scan/livez": { - "name": "livez", - "type": "unlimited" - }, - "/api/scan/readyz": { - "name": "readyz", - "type": "unlimited" - }, - "/api/scan/status": { - "name": "status", - "type": "unlimited" - }, - "/api/scan/v0/acs": { - "fillInterval": "60s", - "maxTokens": 500, - "name": "acs", - "perIpLimits": { - "fillInterval": "60s", - "maxTokens": 10, - "tokensPerFill": 5 - }, - "tokensPerFill": 500, - "type": "limited" - }, - "/api/scan/v0/active-synchronizer-serial": { - "name": "activeSynchronizerSerial", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/previous-sv-reward-weight": { - "name": "sv-previous-sv-reward-weight", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voterequests": { - "name": "sv-voterequests", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voteresults": { - "name": "sv-voteresults", - "type": "unlimited" - }, - "/api/scan/v0/admin/validator": { - "name": "validator-licenses", - "type": "unlimited" - }, - "/api/scan/v0/amulet-config-for-round": { - "name": "amulet-config-for-round", - "type": "banned" - }, - "/api/scan/v0/amulet-price": { - "name": "amulet-price-votes", - "type": "unlimited" - }, - "/api/scan/v0/amulet-rules": { - "name": "amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/ans-entries": { - "name": "ans-entries", - "type": "unlimited" - }, - "/api/scan/v0/ans-rules": { - "name": "ans-rules", - "type": "unlimited" - }, - "/api/scan/v0/backfilling": { - "name": "backfilling", - "type": "unlimited" - }, - "/api/scan/v0/closed-rounds": { - "name": "closed-rounds", - "type": "unlimited" - }, - "/api/scan/v0/domains": { - "name": "domains", - "type": "unlimited" - }, - "/api/scan/v0/dso": { - "name": "dso-info", - "type": "unlimited" - }, - "/api/scan/v0/dso-party-id": { - "name": "dso-party-id", - "type": "unlimited" - }, - "/api/scan/v0/dso-sequencers": { - "name": "dso-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/events": { - "name": "events", - "type": "unlimited" - }, - "/api/scan/v0/external-party-amulet-rules": { - "name": "external-party-amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/feature-support": { - "name": "feature-support", - "type": "unlimited" - }, - "/api/scan/v0/featured-apps": { - "name": "featured-apps", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/acs": { - "name": "listBulkAcsSnapshotObjects", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/checksums": { - "name": "getBulkHistoryChecksums", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/updates": { - "name": "listBulkUpdateHistoryObjects", - "type": "unlimited" - }, - "/api/scan/v0/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v0/internal/reward-accounting-process": { - "name": "reward-accounting-process", - "type": "unlimited" - }, - "/api/scan/v0/lsu": { - "name": "lsu", - "type": "unlimited" - }, - "/api/scan/v0/migrations": { - "name": "migrations-schedule", - "type": "unlimited" - }, - "/api/scan/v0/open-and-issuing-mining-rounds": { - "name": "open-and-issuing-mining-rounds", - "type": "unlimited" - }, - "/api/scan/v0/roll-forward-lsu": { - "name": "rollForwardLsu", - "type": "unlimited" - }, - "/api/scan/v0/scans": { - "name": "scans", - "type": "unlimited" - }, - "/api/scan/v0/splice-instance-names": { - "name": "splice-instance-names", - "type": "unlimited" - }, - "/api/scan/v0/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v0/sv-bft-sequencers": { - "name": "bft-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-bootstrapping-transactions": { - "name": "synchronizer-bootstrapping-transactions", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-identities": { - "name": "synchronizer-identities", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command": { - "name": "transfer-command-status", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command-counter": { - "name": "transfer-command-counter", - "type": "unlimited" - }, - "/api/scan/v0/transfer-preapprovals": { - "name": "transfer-preapprovals", - "type": "unlimited" - }, - "/api/scan/v0/unclaimed-development-fund-coupons": { - "name": "unclaimed-development-fund-coupons", - "type": "unlimited" - }, - "/api/scan/v0/updates": { - "name": "v0-updates", - "type": "unlimited" - }, - "/api/scan/v0/validators": { - "name": "validators", - "type": "unlimited" - }, - "/api/scan/v0/voterequest": { - "name": "voterequest", - "type": "unlimited" - }, - "/api/scan/v0/voterequests": { - "name": "voterequests", - "type": "unlimited" - }, - "/api/scan/v1/domains": { - "name": "domains-v1", - "type": "unlimited" - }, - "/api/scan/v1/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v1/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v1/updates": { - "name": "v1-updates", - "type": "banned" - }, - "/api/scan/v2/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v2/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v2/updates": { - "name": "v2-updates", - "type": "unlimited" - }, - "/api/scan/version": { - "name": "version", - "type": "unlimited" - }, "/registry/allocation-instruction/v1/allocation-factory": { "fillInterval": "60s", "maxTokens": 720, @@ -1923,240 +1701,18 @@ "appLabel": "scan-app", "globalLimits": { "fillInterval": "60s", - "maxTokens": 2147483647, - "tokensPerFill": 2147483647 + "maxTokens": 10000, + "tokensPerFill": 10000 + }, + "globalPerIpLimits": { + "fillInterval": "60s", + "maxTokens": 1000, + "tokensPerFill": 1000 }, "inboundPort": 5012, "namespace": "sv-da-1", + "protocol": "http", "rateLimits": { - "/api/scan/livez": { - "name": "livez", - "type": "unlimited" - }, - "/api/scan/readyz": { - "name": "readyz", - "type": "unlimited" - }, - "/api/scan/status": { - "name": "status", - "type": "unlimited" - }, - "/api/scan/v0/acs": { - "fillInterval": "60s", - "maxTokens": 500, - "name": "acs", - "perIpLimits": { - "fillInterval": "60s", - "maxTokens": 10, - "tokensPerFill": 5 - }, - "tokensPerFill": 500, - "type": "limited" - }, - "/api/scan/v0/active-synchronizer-serial": { - "name": "activeSynchronizerSerial", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/previous-sv-reward-weight": { - "name": "sv-previous-sv-reward-weight", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voterequests": { - "name": "sv-voterequests", - "type": "unlimited" - }, - "/api/scan/v0/admin/sv/voteresults": { - "name": "sv-voteresults", - "type": "unlimited" - }, - "/api/scan/v0/admin/validator": { - "name": "validator-licenses", - "type": "unlimited" - }, - "/api/scan/v0/amulet-config-for-round": { - "name": "amulet-config-for-round", - "type": "banned" - }, - "/api/scan/v0/amulet-price": { - "name": "amulet-price-votes", - "type": "unlimited" - }, - "/api/scan/v0/amulet-rules": { - "name": "amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/ans-entries": { - "name": "ans-entries", - "type": "unlimited" - }, - "/api/scan/v0/ans-rules": { - "name": "ans-rules", - "type": "unlimited" - }, - "/api/scan/v0/backfilling": { - "name": "backfilling", - "type": "unlimited" - }, - "/api/scan/v0/closed-rounds": { - "name": "closed-rounds", - "type": "unlimited" - }, - "/api/scan/v0/domains": { - "name": "domains", - "type": "unlimited" - }, - "/api/scan/v0/dso": { - "name": "dso-info", - "type": "unlimited" - }, - "/api/scan/v0/dso-party-id": { - "name": "dso-party-id", - "type": "unlimited" - }, - "/api/scan/v0/dso-sequencers": { - "name": "dso-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/events": { - "name": "events", - "type": "unlimited" - }, - "/api/scan/v0/external-party-amulet-rules": { - "name": "external-party-amulet-rules", - "type": "unlimited" - }, - "/api/scan/v0/feature-support": { - "name": "feature-support", - "type": "unlimited" - }, - "/api/scan/v0/featured-apps": { - "name": "featured-apps", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/acs": { - "name": "listBulkAcsSnapshotObjects", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/checksums": { - "name": "getBulkHistoryChecksums", - "type": "unlimited" - }, - "/api/scan/v0/history/bulk/updates": { - "name": "listBulkUpdateHistoryObjects", - "type": "unlimited" - }, - "/api/scan/v0/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v0/internal/reward-accounting-process": { - "name": "reward-accounting-process", - "type": "unlimited" - }, - "/api/scan/v0/lsu": { - "name": "lsu", - "type": "unlimited" - }, - "/api/scan/v0/migrations": { - "name": "migrations-schedule", - "type": "unlimited" - }, - "/api/scan/v0/open-and-issuing-mining-rounds": { - "name": "open-and-issuing-mining-rounds", - "type": "unlimited" - }, - "/api/scan/v0/roll-forward-lsu": { - "name": "rollForwardLsu", - "type": "unlimited" - }, - "/api/scan/v0/scans": { - "name": "scans", - "type": "unlimited" - }, - "/api/scan/v0/splice-instance-names": { - "name": "splice-instance-names", - "type": "unlimited" - }, - "/api/scan/v0/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v0/sv-bft-sequencers": { - "name": "bft-sequencers", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-bootstrapping-transactions": { - "name": "synchronizer-bootstrapping-transactions", - "type": "unlimited" - }, - "/api/scan/v0/synchronizer-identities": { - "name": "synchronizer-identities", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command": { - "name": "transfer-command-status", - "type": "unlimited" - }, - "/api/scan/v0/transfer-command-counter": { - "name": "transfer-command-counter", - "type": "unlimited" - }, - "/api/scan/v0/transfer-preapprovals": { - "name": "transfer-preapprovals", - "type": "unlimited" - }, - "/api/scan/v0/unclaimed-development-fund-coupons": { - "name": "unclaimed-development-fund-coupons", - "type": "unlimited" - }, - "/api/scan/v0/updates": { - "name": "v0-updates", - "type": "unlimited" - }, - "/api/scan/v0/validators": { - "name": "validators", - "type": "unlimited" - }, - "/api/scan/v0/voterequest": { - "name": "voterequest", - "type": "unlimited" - }, - "/api/scan/v0/voterequests": { - "name": "voterequests", - "type": "unlimited" - }, - "/api/scan/v1/domains": { - "name": "domains-v1", - "type": "unlimited" - }, - "/api/scan/v1/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v1/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v1/updates": { - "name": "v1-updates", - "type": "banned" - }, - "/api/scan/v2/holdings": { - "name": "holdings", - "type": "unlimited" - }, - "/api/scan/v2/state": { - "name": "state", - "type": "unlimited" - }, - "/api/scan/v2/updates": { - "name": "v2-updates", - "type": "unlimited" - }, - "/api/scan/version": { - "name": "version", - "type": "unlimited" - }, "/registry/allocation-instruction/v1/allocation-factory": { "fillInterval": "60s", "maxTokens": 720, @@ -2996,10 +2552,104 @@ "listener": { "filterChain": { "filter": { - "name": "envoy.filters.network.http_connection_manager" + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" } } } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } }, "patch": { "operation": "INSERT_BEFORE", @@ -3009,7 +2659,7 @@ "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", "value": { - "stat_prefix": "http_local_rate_limiter" + "stat_prefix": "http_local_rate_limiter_global" } } } @@ -3033,50 +2683,6 @@ "value": { "route": { "rate_limits": [ - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - } - ] - }, - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - }, - { - "masked_remote_address": { - "v4_prefix_mask_len": 32, - "v6_prefix_mask_len": 128 - } - } - ] - }, { "actions": [ { @@ -3266,6 +2872,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v1" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } } ] } @@ -3285,6 +2899,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v1" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } } ] } @@ -3486,6 +3108,14 @@ "ignore_case": true, "prefix": "/registry/allocation-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } } ] } @@ -3505,7 +3135,15 @@ "ignore_case": true, "prefix": "/registry/allocation-instruction/v2" } - } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } + } ] } }, @@ -3574,6 +3212,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } } ] } @@ -3593,6 +3239,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } } ] } @@ -3604,47 +3258,92 @@ } } ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] } ] }, "typed_per_filter_config": { "envoy.filters.http.local_ratelimit": { "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, "descriptors": [ { "entries": [ { "key": "header_match", - "value": "acs" + "value": "registry-allocations" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 500, - "tokens_per_fill": 500 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "acs" - }, - { - "key": "masked_remote_address" + "value": "registry-metadata-info" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 10, - "tokens_per_fill": 5 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocations" + "value": "registry-metadata-instruments" } ], "token_bucket": { @@ -3657,23 +3356,20 @@ "entries": [ { "key": "header_match", - "value": "registry-allocations" - }, - { - "key": "masked_remote_address" + "value": "registry-allocation-factory" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-info" + "value": "registry-transfer-instruction" } ], "token_bucket": { @@ -3686,40 +3382,33 @@ "entries": [ { "key": "header_match", - "value": "registry-metadata-info" - }, - { - "key": "masked_remote_address", - "value": "192.68.78.50/32" + "value": "registry-transfer-factory" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 250, - "tokens_per_fill": 250 + "max_tokens": 1440, + "tokens_per_fill": 1440 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-info" - }, - { - "key": "masked_remote_address" + "value": "registry-settlement-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-instruments" + "value": "registry-allocations-v2" } ], "token_bucket": { @@ -3732,23 +3421,20 @@ "entries": [ { "key": "header_match", - "value": "registry-metadata-instruments" - }, - { - "key": "masked_remote_address" + "value": "registry-allocation-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-factory" + "value": "registry-allocation-instruction-v2" } ], "token_bucket": { @@ -3761,23 +3447,20 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-factory" - }, - { - "key": "masked_remote_address" + "value": "registry-transfer-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-transfer-instruction" + "value": "registry-transfer-instruction-v2" } ], "token_bucket": { @@ -3785,12 +3468,50 @@ "max_tokens": 720, "tokens_per_fill": 720 } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ { "entries": [ { "key": "header_match", - "value": "registry-transfer-instruction" + "value": "registry-allocations" }, { "key": "masked_remote_address" @@ -3806,20 +3527,24 @@ "entries": [ { "key": "header_match", - "value": "registry-transfer-factory" + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address", + "value": "192.68.78.50/32" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 1440, - "tokens_per_fill": 1440 + "max_tokens": 250, + "tokens_per_fill": 250 } }, { "entries": [ { "key": "header_match", - "value": "registry-transfer-factory" + "value": "registry-metadata-info" }, { "key": "masked_remote_address" @@ -3827,28 +3552,15 @@ ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 240, - "tokens_per_fill": 240 - } - }, - { - "entries": [ - { - "key": "header_match", - "value": "registry-settlement-factory-v2" - } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-settlement-factory-v2" + "value": "registry-metadata-instruments" }, { "key": "masked_remote_address" @@ -3864,20 +3576,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocations-v2" + "value": "registry-allocation-factory" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocations-v2" + "value": "registry-transfer-instruction" }, { "key": "masked_remote_address" @@ -3893,20 +3608,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-factory-v2" + "value": "registry-transfer-factory" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 240, + "tokens_per_fill": 240 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-factory-v2" + "value": "registry-settlement-factory-v2" }, { "key": "masked_remote_address" @@ -3922,20 +3640,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-instruction-v2" + "value": "registry-allocations-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-instruction-v2" + "value": "registry-allocation-factory-v2" }, { "key": "masked_remote_address" @@ -3951,13 +3672,16 @@ "entries": [ { "key": "header_match", - "value": "registry-transfer-factory-v2" + "value": "registry-allocation-instruction-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { @@ -3976,19 +3700,6 @@ "tokens_per_fill": 120 } }, - { - "entries": [ - { - "key": "header_match", - "value": "registry-transfer-instruction-v2" - } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 - } - }, { "entries": [ { @@ -4022,37 +3733,88 @@ "runtime_key": "local_rate_limit_enforced" }, "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, "response_headers_to_add": [ { "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "x-local-rate-limit", - "value": "true" + "value": "endpoint_per_ip" } } ], - "stat_prefix": "http_local_rate_limiter", + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", "token_bucket": { "fill_interval": "60s", - "max_tokens": 2147483647, - "tokens_per_fill": 2147483647 + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 } - } - } - } - } - } - ], - "workloadSelector": { - "labels": { - "app": "scan-app" - } - } - } - }, - "name": "sv-1-scan-app-rate-limit", - "provider": "", - "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ + { + "entries": [ + { + "key": "masked_remote_address" + } + ], + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 1000, + "tokens_per_fill": 1000 + } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + } + } + } + } + } + ], + "workloadSelector": { + "labels": { + "app": "scan-app" + } + } + } + }, + "name": "sv-1-scan-app-rate-limit", + "provider": "", + "type": "kubernetes:networking.istio.io/v1alpha3:EnvoyFilter" }, { "custom": true, @@ -5376,10 +5138,104 @@ "listener": { "filterChain": { "filter": { - "name": "envoy.filters.network.http_connection_manager" + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint_per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.endpoint", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_endpoint" + } + } + } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } + }, + "patch": { + "operation": "INSERT_BEFORE", + "value": { + "name": "envoy.filters.http.local_ratelimit.per_ip", + "typed_config": { + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", + "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "value": { + "stat_prefix": "http_local_rate_limiter_per_ip" } } } + } + }, + { + "applyTo": "HTTP_FILTER", + "match": { + "context": "SIDECAR_INBOUND", + "listener": { + "filterChain": { + "filter": { + "name": "envoy.filters.network.http_connection_manager", + "subFilter": { + "name": "envoy.filters.http.router" + } + } + }, + "portNumber": 5012 + } }, "patch": { "operation": "INSERT_BEFORE", @@ -5389,7 +5245,7 @@ "@type": "type.googleapis.com/udpa.type.v1.TypedStruct", "type_url": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", "value": { - "stat_prefix": "http_local_rate_limiter" + "stat_prefix": "http_local_rate_limiter_global" } } } @@ -5413,50 +5269,6 @@ "value": { "route": { "rate_limits": [ - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - } - ] - }, - { - "actions": [ - { - "header_value_match": { - "descriptor_value": "acs", - "expect_match": true, - "headers": [ - { - "name": ":path", - "string_match": { - "ignore_case": true, - "prefix": "/api/scan/v0/acs" - } - } - ] - } - }, - { - "masked_remote_address": { - "v4_prefix_mask_len": 32, - "v6_prefix_mask_len": 128 - } - } - ] - }, { "actions": [ { @@ -5646,6 +5458,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v1" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } } ] } @@ -5665,6 +5485,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v1" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v1/transfer-factory" + } } ] } @@ -5866,6 +5694,14 @@ "ignore_case": true, "prefix": "/registry/allocation-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } } ] } @@ -5885,6 +5721,14 @@ "ignore_case": true, "prefix": "/registry/allocation-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/allocation-instruction/v2/allocation-factory" + } } ] } @@ -5954,6 +5798,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } } ] } @@ -5973,6 +5825,14 @@ "ignore_case": true, "prefix": "/registry/transfer-instruction/v2" } + }, + { + "invert_match": true, + "name": ":path", + "string_match": { + "ignore_case": true, + "prefix": "/registry/transfer-instruction/v2/transfer-factory" + } } ] } @@ -5984,47 +5844,92 @@ } } ] + }, + { + "actions": [ + { + "masked_remote_address": { + "v4_prefix_mask_len": 32, + "v6_prefix_mask_len": 128 + } + } + ] } ] }, "typed_per_filter_config": { "envoy.filters.http.local_ratelimit": { "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": true, + "descriptors": [], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "global" + } + } + ], + "stat_prefix": "http_local_rate_limiter_global", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 10000, + "tokens_per_fill": 10000 + } + }, + "envoy.filters.http.local_ratelimit.endpoint": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, "descriptors": [ { "entries": [ { "key": "header_match", - "value": "acs" + "value": "registry-allocations" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 500, - "tokens_per_fill": 500 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "acs" - }, - { - "key": "masked_remote_address" + "value": "registry-metadata-info" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 10, - "tokens_per_fill": 5 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocations" + "value": "registry-metadata-instruments" } ], "token_bucket": { @@ -6037,23 +5942,20 @@ "entries": [ { "key": "header_match", - "value": "registry-allocations" - }, - { - "key": "masked_remote_address" + "value": "registry-allocation-factory" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-info" + "value": "registry-transfer-instruction" } ], "token_bucket": { @@ -6066,40 +5968,33 @@ "entries": [ { "key": "header_match", - "value": "registry-metadata-info" - }, - { - "key": "masked_remote_address", - "value": "192.68.78.50/32" + "value": "registry-transfer-factory" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 250, - "tokens_per_fill": 250 + "max_tokens": 1440, + "tokens_per_fill": 1440 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-info" - }, - { - "key": "masked_remote_address" + "value": "registry-settlement-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-metadata-instruments" + "value": "registry-allocations-v2" } ], "token_bucket": { @@ -6112,23 +6007,20 @@ "entries": [ { "key": "header_match", - "value": "registry-metadata-instruments" - }, - { - "key": "masked_remote_address" + "value": "registry-allocation-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-factory" + "value": "registry-allocation-instruction-v2" } ], "token_bucket": { @@ -6141,23 +6033,20 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-factory" - }, - { - "key": "masked_remote_address" + "value": "registry-transfer-factory-v2" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 720, + "tokens_per_fill": 720 } }, { "entries": [ { "key": "header_match", - "value": "registry-transfer-instruction" + "value": "registry-transfer-instruction-v2" } ], "token_bucket": { @@ -6165,12 +6054,50 @@ "max_tokens": 720, "tokens_per_fill": 720 } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.endpoint_per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ { "entries": [ { "key": "header_match", - "value": "registry-transfer-instruction" + "value": "registry-allocations" }, { "key": "masked_remote_address" @@ -6186,20 +6113,24 @@ "entries": [ { "key": "header_match", - "value": "registry-transfer-factory" + "value": "registry-metadata-info" + }, + { + "key": "masked_remote_address", + "value": "192.68.78.50/32" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 1440, - "tokens_per_fill": 1440 + "max_tokens": 250, + "tokens_per_fill": 250 } }, { "entries": [ { "key": "header_match", - "value": "registry-transfer-factory" + "value": "registry-metadata-info" }, { "key": "masked_remote_address" @@ -6207,28 +6138,15 @@ ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 240, - "tokens_per_fill": 240 - } - }, - { - "entries": [ - { - "key": "header_match", - "value": "registry-settlement-factory-v2" - } - ], - "token_bucket": { - "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-settlement-factory-v2" + "value": "registry-metadata-instruments" }, { "key": "masked_remote_address" @@ -6244,20 +6162,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocations-v2" + "value": "registry-allocation-factory" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocations-v2" + "value": "registry-transfer-instruction" }, { "key": "masked_remote_address" @@ -6273,20 +6194,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-factory-v2" + "value": "registry-transfer-factory" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 240, + "tokens_per_fill": 240 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-factory-v2" + "value": "registry-settlement-factory-v2" }, { "key": "masked_remote_address" @@ -6302,20 +6226,23 @@ "entries": [ { "key": "header_match", - "value": "registry-allocation-instruction-v2" + "value": "registry-allocations-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { "entries": [ { "key": "header_match", - "value": "registry-allocation-instruction-v2" + "value": "registry-allocation-factory-v2" }, { "key": "masked_remote_address" @@ -6331,13 +6258,16 @@ "entries": [ { "key": "header_match", - "value": "registry-transfer-factory-v2" + "value": "registry-allocation-instruction-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } }, { @@ -6361,28 +6291,65 @@ { "key": "header_match", "value": "registry-transfer-instruction-v2" + }, + { + "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 720, - "tokens_per_fill": 720 + "max_tokens": 120, + "tokens_per_fill": 120 } + } + ], + "enable_x_ratelimit_headers": "DRAFT_VERSION_03", + "filter_enabled": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 }, + "runtime_key": "local_rate_limit_enabled" + }, + "filter_enforced": { + "default_value": { + "denominator": "HUNDRED", + "numerator": 100 + }, + "runtime_key": "local_rate_limit_enforced" + }, + "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, + "response_headers_to_add": [ + { + "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", + "header": { + "key": "x-local-rate-limit", + "value": "endpoint_per_ip" + } + } + ], + "stat_prefix": "http_local_rate_limiter_endpoint_per_ip", + "token_bucket": { + "fill_interval": "60s", + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 + } + }, + "envoy.filters.http.local_ratelimit.per_ip": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "always_consume_default_token_bucket": false, + "descriptors": [ { "entries": [ - { - "key": "header_match", - "value": "registry-transfer-instruction-v2" - }, { "key": "masked_remote_address" } ], "token_bucket": { "fill_interval": "60s", - "max_tokens": 120, - "tokens_per_fill": 120 + "max_tokens": 1000, + "tokens_per_fill": 1000 } } ], @@ -6402,20 +6369,21 @@ "runtime_key": "local_rate_limit_enforced" }, "max_dynamic_descriptors": 10000, + "rate_limited_as_resource_exhausted": false, "response_headers_to_add": [ { "append_action": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "x-local-rate-limit", - "value": "true" + "value": "per_ip" } } ], - "stat_prefix": "http_local_rate_limiter", + "stat_prefix": "http_local_rate_limiter_per_ip", "token_bucket": { "fill_interval": "60s", - "max_tokens": 2147483647, - "tokens_per_fill": 2147483647 + "max_tokens": 4294967295, + "tokens_per_fill": 4294967295 } } } diff --git a/cluster/helm/cn-docs/values.schema.json b/cluster/helm/cn-docs/values.schema.json index 6222a142122..98374cfcd07 100644 --- a/cluster/helm/cn-docs/values.schema.json +++ b/cluster/helm/cn-docs/values.schema.json @@ -29,6 +29,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "networkName": { "type": "string" }, diff --git a/cluster/helm/splice-cometbft/templates/deployment.yaml b/cluster/helm/splice-cometbft/templates/deployment.yaml index 0bf5d3a513a..cb5dc6066ae 100644 --- a/cluster/helm/splice-cometbft/templates/deployment.yaml +++ b/cluster/helm/splice-cometbft/templates/deployment.yaml @@ -202,6 +202,9 @@ spec: emptyDir: sizeLimit: 1Mi {{- end }} + {{- with $.Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with $.Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cluster/helm/splice-cometbft/values-template.yaml b/cluster/helm/splice-cometbft/values-template.yaml index ce844eb38cc..7d4f7817bd7 100644 --- a/cluster/helm/splice-cometbft/values-template.yaml +++ b/cluster/helm/splice-cometbft/values-template.yaml @@ -97,6 +97,9 @@ metrics: # k8s tolerations for all deployed pods (optional) # tolerations: +# k8s priorityClassName for all deployed pods (optional) +# priorityClassName: + enableAntiAffinity: true logLevel: info @@ -128,10 +131,10 @@ watchdog: imageName: "cometbft-watchdog" resources: limits: - memory: 128Mi + memory: 512Mi requests: - cpu: 0.05 - memory: 64Mi + cpu: 0.5 + memory: 256Mi securityContexts: pod: diff --git a/cluster/helm/splice-cometbft/values.schema.json b/cluster/helm/splice-cometbft/values.schema.json index a3b710be719..76b91c62f24 100644 --- a/cluster/helm/splice-cometbft/values.schema.json +++ b/cluster/helm/splice-cometbft/values.schema.json @@ -37,6 +37,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "genesis": { "type": "object", "required": [ diff --git a/cluster/helm/splice-global-domain/templates/mediator.yaml b/cluster/helm/splice-global-domain/templates/mediator.yaml index 952bcac9d68..f40ece2f5c6 100644 --- a/cluster/helm/splice-global-domain/templates/mediator.yaml +++ b/cluster/helm/splice-global-domain/templates/mediator.yaml @@ -155,6 +155,9 @@ spec: echo "trying to create postgres database {{ .Values.mediator.persistence.databaseName }}, last error: $errmsg"; sleep 2; done + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -227,5 +230,5 @@ data: {{ .Files.Get "files/logback.xml" | indent 4 }} --- {{- if .Values.enablePostgresMetrics }} -{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.mediator.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.mediator.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- end}} diff --git a/cluster/helm/splice-global-domain/templates/required.yaml b/cluster/helm/splice-global-domain/templates/required.yaml index cac166292e1..c526c0d234f 100644 --- a/cluster/helm/splice-global-domain/templates/required.yaml +++ b/cluster/helm/splice-global-domain/templates/required.yaml @@ -11,10 +11,7 @@ {{ $_ := required ".Values.mediator.persistence.host is required." .Values.mediator.persistence.host }} {{ $_ := required ".Values.mediator.persistence.secretName is required" .Values.mediator.persistence.secretName }} {{ $_ := required ".Values.sequencer.driver.type is required." (.Values.sequencer.driver).type }} -{{- if eq .Values.sequencer.driver.type "postgres"}} -{{ $_ := required ".Values.sequencer.driver.address is required." (.Values.sequencer.driver).address }} -{{ $_ := required ".Values.sequencer.driver.password is required." (.Values.sequencer.driver).password }} -{{- else if eq .Values.sequencer.driver.type "cometbft"}} +{{- if eq .Values.sequencer.driver.type "cometbft"}} {{ $_ := required ".Values.sequencer.driver.host is required." (.Values.sequencer.driver).host }} {{ $_ := required ".Values.sequencer.driver.port is required." (.Values.sequencer.driver).port }} {{- else if eq .Values.sequencer.driver.type "cantonbft"}} diff --git a/cluster/helm/splice-global-domain/templates/sequencer.yaml b/cluster/helm/splice-global-domain/templates/sequencer.yaml index 41a147a590d..7e702abbc3f 100644 --- a/cluster/helm/splice-global-domain/templates/sequencer.yaml +++ b/cluster/helm/splice-global-domain/templates/sequencer.yaml @@ -71,31 +71,7 @@ spec: - name: CANTON_SEQUENCER_POSTGRES_DB value: {{ .Values.sequencer.persistence.databaseName }} {{- end }} - {{- if eq .Values.sequencer.driver.type "postgres"}} - - name: ADDITIONAL_CONFIG_SEQUENCER_DRIVER_REFERENCE - value: | - canton.sequencers.sequencer.sequencer { - config { - storage = ${_storage} - storage.config.properties.serverName = ${?SEQUENCER_DRIVER_DATABASE_ADDRESS} - storage.config.properties.password = ${?SEQUENCER_DRIVER_DATABASE_PASSWORD} - storage.config.properties.currentSchema = "sequencer_driver" - storage.config.properties.databaseName = ${?CANTON_SEQUENCER_POSTGRES_DB} - } - type = "reference" - } - - name: SEQUENCER_DRIVER_DATABASE_ADDRESS - value: {{ .Values.sequencer.driver.address }} - - name: SEQUENCER_DRIVER_DATABASE_PASSWORD - {{- if ((.Values.sequencer.persistence).secretOverrides).postgresPassword }} - value: {{ .Values.sequencer.persistence.secretOverrides.postgresPassword | quote }} - {{- else }} - valueFrom: - secretKeyRef: - name: {{ .Values.sequencer.persistence.secretName }} - key: postgresPassword - {{- end }} - {{- else if eq .Values.sequencer.driver.type "cometbft"}} + {{- if eq .Values.sequencer.driver.type "cometbft"}} - name: ADDITIONAL_CONFIG_SEQUENCER_DRIVER_COMETBFT value: | canton.sequencers.sequencer.sequencer { @@ -274,6 +250,9 @@ spec: psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" --username="$DB_USER" --dbname=cantonnet -c "CREATE DATABASE \"${DB_NAME}\""; echo "Created empty database ${DB_NAME} on ${DB_HOST}."; {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -323,11 +302,11 @@ spec: protocol: TCP --- {{- if .Values.enablePostgresMetrics }} -{{- include "splice-util-lib.postgres-metrics" (dict "name" (print "postgres-" (.Values.sequencer.persistence.databaseName | replace "_" "-" ) "-e") "namespace" .Release.Namespace "persistence" .Values.sequencer.persistence "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "name" (print "postgres-" (.Values.sequencer.persistence.databaseName | replace "_" "-" ) "-e") "namespace" .Release.Namespace "persistence" .Values.sequencer.persistence "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- if and (eq .Values.sequencer.driver.type "cantonbft") ((.Values.sequencer.driver).persistence) (((.Values.sequencer.driver).persistence).databaseName) (ne .Values.sequencer.driver.persistence.databaseName .Values.sequencer.persistence.databaseName) }} {{- $driverPersistence := dict "postgresName" (print .Values.sequencer.persistence.postgresName "-driver") "databaseName" .Values.sequencer.driver.persistence.databaseName "host" (.Values.sequencer.driver.persistence.host | default .Values.sequencer.persistence.host) "port" (.Values.sequencer.driver.persistence.port | default .Values.sequencer.persistence.port) "secretName" (.Values.sequencer.driver.persistence.secretName | default .Values.sequencer.persistence.secretName) }} --- -{{- include "splice-util-lib.postgres-metrics" (dict "namespace" .Release.Namespace "persistence" $driverPersistence "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "namespace" .Release.Namespace "persistence" $driverPersistence "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- end }} {{- end}} {{- if .Values.pvc }} diff --git a/cluster/helm/splice-global-domain/tests/mediator_test.yaml b/cluster/helm/splice-global-domain/tests/mediator_test.yaml index 07691efb2d4..1318cf3bab5 100644 --- a/cluster/helm/splice-global-domain/tests/mediator_test.yaml +++ b/cluster/helm/splice-global-domain/tests/mediator_test.yaml @@ -297,3 +297,33 @@ tests: capabilities: drop: - ALL + + - it: "propagates priorityClassName to the postgres-metrics deployment" + set: + enablePostgresMetrics: true + priorityClassName: high-priority + mediator: + persistence: + postgresName: mediator-pg + databaseName: mediator_db + documentSelector: + path: spec.template.metadata.labels.app + value: pge-mediator-pg-mediator-db + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: high-priority + + - it: "omits priorityClassName on the postgres-metrics deployment when unset" + set: + enablePostgresMetrics: true + mediator: + persistence: + postgresName: mediator-pg + databaseName: mediator_db + documentSelector: + path: spec.template.metadata.labels.app + value: pge-mediator-pg-mediator-db + asserts: + - notExists: + path: spec.template.spec.priorityClassName diff --git a/cluster/helm/splice-global-domain/tests/sequencer_test.yaml b/cluster/helm/splice-global-domain/tests/sequencer_test.yaml index 1d778015b3a..bdafe18ddf3 100644 --- a/cluster/helm/splice-global-domain/tests/sequencer_test.yaml +++ b/cluster/helm/splice-global-domain/tests/sequencer_test.yaml @@ -355,7 +355,7 @@ tests: - it: "allows overriding sequencer base database password with raw strings" set: - sequencer.driver.type: "postgres" + sequencer.driver.type: "cantonbft" sequencer.driver.address: "mock-driver-address" sequencer.persistence.secretOverrides.postgresPassword: "vault:kv/data/sequencer/postgres-password" documentSelector: @@ -365,9 +365,6 @@ tests: - matchRegex: path: spec.template.spec.containers[?(@.name=='sequencer')].env[?(@.name=='CANTON_DOMAIN_POSTGRES_PASSWORD')].value pattern: "vault:kv/data/sequencer/postgres-password" - - matchRegex: - path: spec.template.spec.containers[?(@.name=='sequencer')].env[?(@.name=='SEQUENCER_DRIVER_DATABASE_PASSWORD')].value - pattern: "vault:kv/data/sequencer/postgres-password" - matchRegex: path: spec.template.spec.initContainers[?(@.name=='pg-init')].env[?(@.name=='PGPASSWORD')].value pattern: "vault:kv/data/sequencer/postgres-password" diff --git a/cluster/helm/splice-global-domain/values-template.yaml b/cluster/helm/splice-global-domain/values-template.yaml index d4b033a3166..a69cdffed53 100644 --- a/cluster/helm/splice-global-domain/values-template.yaml +++ b/cluster/helm/splice-global-domain/values-template.yaml @@ -65,6 +65,9 @@ metrics: # k8s tolerations for all deployed pods (optional) # tolerations: +# k8s priorityClassName for all deployed pods (optional) +# priorityClassName: + enableAntiAffinity: true # should stakater reloader annotation be added (note: reloader needs to be installed separately) diff --git a/cluster/helm/splice-global-domain/values.schema.json b/cluster/helm/splice-global-domain/values.schema.json index e3fe394b069..3535f97c4db 100644 --- a/cluster/helm/splice-global-domain/values.schema.json +++ b/cluster/helm/splice-global-domain/values.schema.json @@ -2,6 +2,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "serviceAccountName": { + "type": "string" + }, + "automountServiceAccountToken": { + "type": "boolean" + }, "securityContexts": { "type": "object", "properties": { diff --git a/cluster/helm/splice-info/values.schema.json b/cluster/helm/splice-info/values.schema.json index 4d46efb7965..bfea2f00c95 100644 --- a/cluster/helm/splice-info/values.schema.json +++ b/cluster/helm/splice-info/values.schema.json @@ -65,6 +65,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "nginxImage": { "type": "string", "description": "The Docker image to use for Nginx. Defaults to 'nginx:latest' if not present." diff --git a/cluster/helm/splice-load-tester/values.schema.json b/cluster/helm/splice-load-tester/values.schema.json index 99aa9c03422..1429d5d3a50 100644 --- a/cluster/helm/splice-load-tester/values.schema.json +++ b/cluster/helm/splice-load-tester/values.schema.json @@ -2,6 +2,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "serviceAccountName": { + "type": "string" + }, + "automountServiceAccountToken": { + "type": "boolean" + }, "securityContexts": { "type": ["object", "null"], "properties": { diff --git a/cluster/helm/splice-participant/templates/participant.yaml b/cluster/helm/splice-participant/templates/participant.yaml index 841041cce46..85b7990f88d 100644 --- a/cluster/helm/splice-participant/templates/participant.yaml +++ b/cluster/helm/splice-participant/templates/participant.yaml @@ -55,6 +55,10 @@ spec: value: {{ .Values.persistence.port | quote }} - name: CANTON_PARTICIPANT_POSTGRES_SCHEMA value: {{ .Values.persistence.schema }} + {{- with .Values.persistence.user }} + - name: CANTON_PARTICIPANT_POSTGRES_USER + value: {{ . | quote }} + {{- end }} - name: CANTON_PARTICIPANT_POSTGRES_PASSWORD {{- if ((.Values.persistence).secretOverrides).postgresPassword }} value: {{ .Values.persistence.secretOverrides.postgresPassword | quote }} @@ -186,7 +190,7 @@ spec: {{- end }} DB_TO_CREATE='{{ .Values.persistence.databaseName }}' - until db_exists=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username=cnadmin --dbname=cantonnet -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_TO_CREATE}'" 2>&1); do + until db_exists=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username={{ .Values.persistence.user | default "cnadmin" }} --dbname={{ .Values.persistence.bootstrapDatabaseName | default "cantonnet" }} -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_TO_CREATE}'" 2>&1); do echo "trying to connect to postgres, last error: ${db_exists}"; sleep 2; done @@ -195,7 +199,7 @@ spec: echo "Database ${DB_TO_CREATE} already exists. Done." else {{- if $conflictCheck }} - existing=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username=cnadmin --dbname=cantonnet -tAc "SELECT datname FROM pg_database WHERE datname LIKE 'participant%'" | paste -sd ', ' -) + existing=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username={{ .Values.persistence.user | default "cnadmin" }} --dbname={{ .Values.persistence.bootstrapDatabaseName | default "cantonnet" }} -tAc "SELECT datname FROM pg_database WHERE datname LIKE 'participant%'" | paste -sd ', ' -) if [ -n "${existing}" ]; then echo "ERROR: Refusing to create participant database '${DB_TO_CREATE}' because the following participant database(s) already exist: ${existing}." >&2 echo " A participant database should only be created during a fresh deployment. Please double-check that this is intended and not a misconfiguration." >&2 @@ -203,7 +207,7 @@ spec: exit 1 fi {{- end }} - until errmsg=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username=cnadmin --dbname=cantonnet -c "create database ${DB_TO_CREATE}" 2>&1); do + until errmsg=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username={{ .Values.persistence.user | default "cnadmin" }} --dbname={{ .Values.persistence.bootstrapDatabaseName | default "cantonnet" }} -c "create database ${DB_TO_CREATE}" 2>&1); do if [[ ${errmsg} == *"already exists"* ]]; then echo "Database ${DB_TO_CREATE} already exists. Done." break @@ -218,6 +222,9 @@ spec: {{ .Values.extraInitContainers | toYaml | nindent 8 }} {{- end }} {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -304,5 +311,5 @@ data: {{ .Files.Get "files/logback.xml" | indent 4 }} --- {{- if .Values.enablePostgresMetrics }} -{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- end}} diff --git a/cluster/helm/splice-participant/tests/participant_test.yaml b/cluster/helm/splice-participant/tests/participant_test.yaml index 90e1fb94931..673c15c68f5 100644 --- a/cluster/helm/splice-participant/tests/participant_test.yaml +++ b/cluster/helm/splice-participant/tests/participant_test.yaml @@ -464,3 +464,33 @@ tests: drop: - ALL + + - it: "sets priorityClassName when provided" + set: + priorityClassName: high-priority + documentSelector: + path: kind + value: Deployment + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: high-priority + + - it: "renders a numeric-looking priorityClassName as a string" + set: + priorityClassName: "42" + documentSelector: + path: kind + value: Deployment + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: "42" + + - it: "omits priorityClassName when not provided" + documentSelector: + path: kind + value: Deployment + asserts: + - notExists: + path: spec.template.spec.priorityClassName diff --git a/cluster/helm/splice-participant/tests/postgres-identity_test.yaml b/cluster/helm/splice-participant/tests/postgres-identity_test.yaml new file mode 100644 index 00000000000..dee1859a6b5 --- /dev/null +++ b/cluster/helm/splice-participant/tests/postgres-identity_test.yaml @@ -0,0 +1,69 @@ +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json +suite: "postgres identity" +templates: + - participant.yaml +set: + # Things we need just to pass the schema + persistence: + host: mock-pg-host + schema: participant + auth: + jwksUrl: "https://mock.com/.well-known/jwks.json" + targetAudience: "mock_audience" +tests: + - it: "defaults the pg-init role and bootstrap database to cnadmin/cantonnet" + documentSelector: + path: kind + value: Deployment + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "--username=cnadmin --dbname=cantonnet" + + - it: "uses persistence.user and persistence.bootstrapDatabaseName when set" + set: + persistence: + host: mock-pg-host + schema: participant + user: appuser + bootstrapDatabaseName: postgres + documentSelector: + path: kind + value: Deployment + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "--username=appuser --dbname=postgres" + - notMatchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "--username=cnadmin|--dbname=cantonnet\\b" + + - it: "passes persistence.user to the canton node itself, not just the init container" + set: + persistence: + host: mock-pg-host + schema: participant + user: appuser + documentSelector: + path: kind + value: Deployment + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: CANTON_PARTICIPANT_POSTGRES_USER + value: appuser + + - it: "omits the canton node DB user when persistence.user is unset" + documentSelector: + path: kind + value: Deployment + asserts: + - notContains: + path: spec.template.spec.containers[0].env + any: true + content: + name: CANTON_PARTICIPANT_POSTGRES_USER diff --git a/cluster/helm/splice-participant/values-template.yaml b/cluster/helm/splice-participant/values-template.yaml index 166522ace81..82ca4ac79f5 100644 --- a/cluster/helm/splice-participant/values-template.yaml +++ b/cluster/helm/splice-participant/values-template.yaml @@ -31,6 +31,10 @@ metrics: persistence: secretName: "postgres-secrets" + # Postgres role the charts connect as, including the pg-init containers (default cnadmin) + # user: cnadmin + # Database the pg-init container connects to in order to CREATE the target database (default cantonnet) + # bootstrapDatabaseName: cantonnet port: 5432 enablePgInitContainer: true initImageName: "postgres:14" @@ -49,6 +53,9 @@ persistence: # k8s tolerations for all deployed pods (optional) # tolerations: +# k8s priorityClassName for all deployed pods (optional) +# priorityClassName: + extraInitContainers: [] # set to true to disable auth (this is highly insecure) diff --git a/cluster/helm/splice-participant/values.schema.json b/cluster/helm/splice-participant/values.schema.json index 6e5f8960185..a3f5cefe10e 100644 --- a/cluster/helm/splice-participant/values.schema.json +++ b/cluster/helm/splice-participant/values.schema.json @@ -33,6 +33,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "imageRepo": { "type": "string" }, @@ -92,6 +95,12 @@ "minimum": 0, "maximum": 65535 }, + "bootstrapDatabaseName": { + "type": "string" + }, + "user": { + "type": "string" + }, "initImageName": { "type": "string" }, @@ -212,6 +221,9 @@ "tolerations": { "type": "array" }, + "priorityClassName": { + "type": "string" + }, "nodeSelector": { "type": "object" }, diff --git a/cluster/helm/splice-party-allocator/values.schema.json b/cluster/helm/splice-party-allocator/values.schema.json index 50d0b8a9834..321f2638b8b 100644 --- a/cluster/helm/splice-party-allocator/values.schema.json +++ b/cluster/helm/splice-party-allocator/values.schema.json @@ -2,6 +2,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "serviceAccountName": { + "type": "string" + }, + "automountServiceAccountToken": { + "type": "boolean" + }, "securityContexts": { "type": ["object", "null"], "properties": { diff --git a/cluster/helm/splice-postgres/tests/postgres_test.yaml b/cluster/helm/splice-postgres/tests/postgres_test.yaml index dd2772d4aaf..f0011eddcc6 100644 --- a/cluster/helm/splice-postgres/tests/postgres_test.yaml +++ b/cluster/helm/splice-postgres/tests/postgres_test.yaml @@ -111,7 +111,7 @@ tests: value: test-postgres - equal: path: spec.template.spec.containers[0].image - value: postgres:14 + value: postgres:14.24-trixie - equal: path: spec.template.spec.containers[0].imagePullPolicy value: IfNotPresent diff --git a/cluster/helm/splice-postgres/values-template.yaml b/cluster/helm/splice-postgres/values-template.yaml index dad72df81d4..a2b059a494b 100644 --- a/cluster/helm/splice-postgres/values-template.yaml +++ b/cluster/helm/splice-postgres/values-template.yaml @@ -8,7 +8,7 @@ # https://docs.canton.network/global-synchronizer/production-operations/validator-postgres-migration imageRepo: "ghcr.io/digital-asset/decentralized-canton-sync/docker" -imageName: "postgres:14" +imageName: "postgres:14.24-trixie" resources: limits: diff --git a/cluster/helm/splice-postgres/values.schema.json b/cluster/helm/splice-postgres/values.schema.json index 29420f9f902..eaf5b9049a4 100644 --- a/cluster/helm/splice-postgres/values.schema.json +++ b/cluster/helm/splice-postgres/values.schema.json @@ -24,6 +24,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "imageRepo": { "type": "string" }, diff --git a/cluster/helm/splice-scan/templates/scan.yaml b/cluster/helm/splice-scan/templates/scan.yaml index 53b219e3e52..0742bef9ae1 100644 --- a/cluster/helm/splice-scan/templates/scan.yaml +++ b/cluster/helm/splice-scan/templates/scan.yaml @@ -335,6 +335,9 @@ spec: echo "trying to create postgres database {{ .Values.persistence.databaseName }}, last error: $errmsg"; sleep 2; done + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -466,6 +469,9 @@ spec: memory: 240Mi limits: memory: 1536Mi + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -505,5 +511,5 @@ data: {{ .Files.Get "files/logback.xml" | indent 4 }} --- {{- if .Values.enablePostgresMetrics }} -{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- end}} diff --git a/cluster/helm/splice-scan/values-template.yaml b/cluster/helm/splice-scan/values-template.yaml index ac36dfb0f3f..c9153086fc8 100644 --- a/cluster/helm/splice-scan/values-template.yaml +++ b/cluster/helm/splice-scan/values-template.yaml @@ -62,6 +62,9 @@ spliceInstanceNames: # k8s tolerations for all deployed pods (optional) # tolerations: +# k8s priorityClassName for all deployed pods (optional) +# priorityClassName: + # set the poll interval used by the UI in milliseconds (optional) # uiPollInterval: diff --git a/cluster/helm/splice-scan/values.schema.json b/cluster/helm/splice-scan/values.schema.json index 770973fd96d..c45eec75c89 100644 --- a/cluster/helm/splice-scan/values.schema.json +++ b/cluster/helm/splice-scan/values.schema.json @@ -53,6 +53,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "imageRepo": { "type": "string" }, @@ -273,6 +276,9 @@ "tolerations": { "type": "array" }, + "priorityClassName": { + "type": "string" + }, "enablePostgresMetrics": { "type": "boolean" }, diff --git a/cluster/helm/splice-splitwell-app/values.schema.json b/cluster/helm/splice-splitwell-app/values.schema.json index 165017a469f..4c0f63ee956 100644 --- a/cluster/helm/splice-splitwell-app/values.schema.json +++ b/cluster/helm/splice-splitwell-app/values.schema.json @@ -2,6 +2,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "serviceAccountName": { + "type": "string" + }, + "automountServiceAccountToken": { + "type": "boolean" + }, "securityContexts": { "type": ["object", "null"], "properties": { diff --git a/cluster/helm/splice-splitwell-web-ui/values.schema.json b/cluster/helm/splice-splitwell-web-ui/values.schema.json index 4115953c94a..05de221b81c 100644 --- a/cluster/helm/splice-splitwell-web-ui/values.schema.json +++ b/cluster/helm/splice-splitwell-web-ui/values.schema.json @@ -24,6 +24,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "imageName": { "type": "string" }, diff --git a/cluster/helm/splice-sv-node/templates/sv-web-ui.yaml b/cluster/helm/splice-sv-node/templates/sv-web-ui.yaml index 6f80afbb43c..120fa3b9725 100644 --- a/cluster/helm/splice-sv-node/templates/sv-web-ui.yaml +++ b/cluster/helm/splice-sv-node/templates/sv-web-ui.yaml @@ -94,6 +94,9 @@ spec: memory: 240Mi limits: memory: 1536Mi + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cluster/helm/splice-sv-node/templates/sv.yaml b/cluster/helm/splice-sv-node/templates/sv.yaml index 21481661b2e..d06620c1b03 100644 --- a/cluster/helm/splice-sv-node/templates/sv.yaml +++ b/cluster/helm/splice-sv-node/templates/sv.yaml @@ -647,6 +647,9 @@ spec: persistentVolumeClaim: claimName: {{ .Release.Name }}-sv-app-pvc {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -724,5 +727,5 @@ data: {{ .Files.Get "files/logback.xml" | indent 4 }} --- {{- if .Values.enablePostgresMetrics }} -{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- end}} diff --git a/cluster/helm/splice-sv-node/values-template.yaml b/cluster/helm/splice-sv-node/values-template.yaml index 59168321d0c..f4b2288a759 100644 --- a/cluster/helm/splice-sv-node/values-template.yaml +++ b/cluster/helm/splice-sv-node/values-template.yaml @@ -78,6 +78,9 @@ spliceInstanceNames: # k8s tolerations for all deployed pods (optional) # tolerations: +# k8s priorityClassName for all deployed pods (optional) +# priorityClassName: + # set the poll interval used by the UI in milliseconds (optional) # uiPollInterval: diff --git a/cluster/helm/splice-sv-node/values.schema.json b/cluster/helm/splice-sv-node/values.schema.json index d71cfefe432..97d56d76e3a 100644 --- a/cluster/helm/splice-sv-node/values.schema.json +++ b/cluster/helm/splice-sv-node/values.schema.json @@ -30,6 +30,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "imageName": { "type": "string" }, @@ -584,6 +587,9 @@ "tolerations": { "type": "array" }, + "priorityClassName": { + "type": "string" + }, "livenessProbeInitialDelaySeconds": { "type": "integer", "minimum": 0 diff --git a/cluster/helm/splice-util-lib/templates/_helpers.tpl b/cluster/helm/splice-util-lib/templates/_helpers.tpl index 5d95cdbba3a..9aba56e5c92 100644 --- a/cluster/helm/splice-util-lib/templates/_helpers.tpl +++ b/cluster/helm/splice-util-lib/templates/_helpers.tpl @@ -67,6 +67,7 @@ valueFrom: {{- $nodeSelector := .nodeSelector }} {{- $affinity := .affinity }} {{- $tolerations := .tolerations }} +{{- $priorityClassName := .priorityClassName }} apiVersion: apps/v1 kind: Deployment metadata: @@ -99,7 +100,7 @@ spec: - name: DATA_SOURCE_PASS_FILE value: /tmp/pwd - name: DATA_SOURCE_USER - value: cnadmin + value: {{ $persistence.user | default "cnadmin" }} - name: DATA_SOURCE_URI value: {{ $persistence.host }}:{{ $persistence.port | default 5432 }}/{{ $.persistence.databaseName }}?sslmode=disable command: @@ -123,10 +124,13 @@ spec: - 'bash' - '-c' - | - until errmsg=$(psql -h {{ $persistence.host }} -p {{ $persistence.port }} --username=cnadmin --dbname={{ $persistence.databaseName }} -p {{ $persistence.port | default 5432 }} -c 'select 1' 2>&1); do + until errmsg=$(psql -h {{ $persistence.host }} -p {{ $persistence.port }} --username={{ $persistence.user | default "cnadmin" }} --dbname={{ $persistence.databaseName }} -p {{ $persistence.port | default 5432 }} -c 'select 1' 2>&1); do echo "Waiting for database {{ $persistence.databaseName }}, at hostname {{ $persistence.host }}, port {{ $persistence.port | default 5432 }} to be accessible. Last error: $errmsg" sleep 2; done + {{- with $priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with $nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -191,9 +195,10 @@ spec: value: {{ .logAsyncFlush | default true | not | quote }} {{- end }} {{- define "splice-util-lib.service-account" -}} -{{- if .serviceAccountName -}} +automountServiceAccountToken: {{ .automountServiceAccountToken | default false }} +{{- if .serviceAccountName }} serviceAccountName: {{ .serviceAccountName }} -{{- end -}} +{{- end }} {{- end -}} # See https://helm.sh/docs/chart_best_practices/labels/#standard-labels {{- define "splice-util-lib.standard-labels" -}} diff --git a/cluster/helm/splice-validator/templates/ans-web-ui.yaml b/cluster/helm/splice-validator/templates/ans-web-ui.yaml index 1d6cf40ebf5..942e960d2e9 100644 --- a/cluster/helm/splice-validator/templates/ans-web-ui.yaml +++ b/cluster/helm/splice-validator/templates/ans-web-ui.yaml @@ -108,6 +108,9 @@ spec: memory: 240Mi limits: memory: 1536Mi + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cluster/helm/splice-validator/templates/validator.yaml b/cluster/helm/splice-validator/templates/validator.yaml index 17f0e6382fd..60d3d702c75 100644 --- a/cluster/helm/splice-validator/templates/validator.yaml +++ b/cluster/helm/splice-validator/templates/validator.yaml @@ -443,7 +443,7 @@ spec: - 'bash' - '-c' - | - until errmsg=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username=cnadmin --dbname=cantonnet -c 'create database {{ .Values.persistence.databaseName }}' 2>&1); do + until errmsg=$(psql -h {{ .Values.persistence.host }} -p {{ .Values.persistence.port }} --username={{ .Values.persistence.user | default "cnadmin" }} --dbname={{ .Values.persistence.bootstrapDatabaseName | default "cantonnet" }} -c 'create database {{ .Values.persistence.databaseName }}' 2>&1); do if [[ $errmsg == *"already exists"* ]]; then echo "Database {{ .Values.persistence.databaseName }} already exists. Done." break @@ -457,6 +457,9 @@ spec: {{ .Values.extraInitContainers | toYaml | nindent 8 }} {{- end }} {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -535,5 +538,5 @@ data: {{ .Files.Get "files/logback.xml" | indent 4 }} --- {{- if .Values.enablePostgresMetrics }} -{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "serviceAccountName" .Values.serviceAccountName ) }} +{{- include "splice-util-lib.postgres-metrics" (dict "persistence" .Values.persistence "namespace" .Release.Namespace "nodeSelector" .Values.nodeSelector "affinity" .Values.affinity "tolerations" .Values.tolerations "priorityClassName" .Values.priorityClassName "serviceAccountName" .Values.serviceAccountName ) }} {{- end}} diff --git a/cluster/helm/splice-validator/templates/wallet-web-ui.yaml b/cluster/helm/splice-validator/templates/wallet-web-ui.yaml index 08c122119dc..ce23703936a 100644 --- a/cluster/helm/splice-validator/templates/wallet-web-ui.yaml +++ b/cluster/helm/splice-validator/templates/wallet-web-ui.yaml @@ -108,6 +108,9 @@ spec: memory: 240Mi limits: memory: 1536Mi + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/cluster/helm/splice-validator/tests/postgres-identity_test.yaml b/cluster/helm/splice-validator/tests/postgres-identity_test.yaml new file mode 100644 index 00000000000..81657492a86 --- /dev/null +++ b/cluster/helm/splice-validator/tests/postgres-identity_test.yaml @@ -0,0 +1,49 @@ +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json +suite: "postgres identity" +templates: + - validator.yaml +set: + # Things we need just to pass the schema + nodeIdentifier: "helm-mock-1-validator" + validatorPartyHint: "helm-mock-1" + validatorWalletUser: "mock-wallet-user-id" + topup: + enabled: false + auth: + jwksUrl: "https://mock.com/.well-known/jwks.json" + audience: "mock_audience" + spliceInstanceNames: + networkName: MockNet + networkFaviconUrl: https://mock.net/favicon.ico + amuletName: Mocklet + amuletNameAcronym: MCK + nameServiceName: Mock Name Service + nameServiceNameAcronym: MNS +tests: + - it: "defaults the pg-init role and bootstrap database to cnadmin/cantonnet" + documentSelector: + path: kind + value: Deployment + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "--username=cnadmin --dbname=cantonnet" + + - it: "uses persistence.user and persistence.bootstrapDatabaseName when set" + set: + persistence: + user: appuser + bootstrapDatabaseName: postgres + documentSelector: + path: kind + value: Deployment + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "--username=appuser --dbname=postgres" + - notMatchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "--username=cnadmin|--dbname=cantonnet\\b" diff --git a/cluster/helm/splice-validator/tests/validator_test.yaml b/cluster/helm/splice-validator/tests/validator_test.yaml index 5db35709c6f..2f305221304 100644 --- a/cluster/helm/splice-validator/tests/validator_test.yaml +++ b/cluster/helm/splice-validator/tests/validator_test.yaml @@ -559,3 +559,32 @@ tests: drop: - ALL + - it: "sets priorityClassName when provided" + set: + priorityClassName: high-priority + documentSelector: + path: kind + value: Deployment + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: high-priority + + - it: "renders a numeric-looking priorityClassName as a string" + set: + priorityClassName: "42" + documentSelector: + path: kind + value: Deployment + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: "42" + + - it: "omits priorityClassName when not provided" + documentSelector: + path: kind + value: Deployment + asserts: + - notExists: + path: spec.template.spec.priorityClassName diff --git a/cluster/helm/splice-validator/values-template.yaml b/cluster/helm/splice-validator/values-template.yaml index beaeadf643e..d6792ee93d1 100644 --- a/cluster/helm/splice-validator/values-template.yaml +++ b/cluster/helm/splice-validator/values-template.yaml @@ -33,6 +33,10 @@ persistence: port: 5432 user: cnadmin secretName: "postgres-secrets" + # Postgres role the charts connect as, including the pg-init containers (default cnadmin) + # user: cnadmin + # Database the pg-init container connects to in order to CREATE the target database (default cantonnet) + # bootstrapDatabaseName: cantonnet enablePgInitContainer: true initImageName: "postgres:14" # Optional: Provide raw strings to override the default Kubernetes secret injection. @@ -62,6 +66,9 @@ enableWallet: true # k8s tolerations for all deployed pods (optional) # tolerations: +# k8s priorityClassName for all deployed pods (optional) +# priorityClassName: + spliceInstanceNames: networkName: # Please provide a value as provided in the docs networkFaviconUrl: # Please provide a value as provided in the docs diff --git a/cluster/helm/splice-validator/values.schema.json b/cluster/helm/splice-validator/values.schema.json index 1111a4f2a84..fa2e6212103 100644 --- a/cluster/helm/splice-validator/values.schema.json +++ b/cluster/helm/splice-validator/values.schema.json @@ -22,6 +22,9 @@ "serviceAccountName": { "type": "string" }, + "automountServiceAccountToken": { + "type": "boolean" + }, "ansWebUi": { "type": "object", "required": ["imageName"], @@ -362,6 +365,9 @@ "secretName": { "type": "string" }, + "bootstrapDatabaseName": { + "type": "string" + }, "initImageName": { "type": "string" }, @@ -418,6 +424,9 @@ "tolerations": { "type": "array" }, + "priorityClassName": { + "type": "string" + }, "auth": { "type": "object", "required": ["audience", "jwksUrl"], diff --git a/cluster/images/canton-mediator/additional-config.conf b/cluster/images/canton-mediator/additional-config.conf index 92064698cb0..65c5c4623df 100644 --- a/cluster/images/canton-mediator/additional-config.conf +++ b/cluster/images/canton-mediator/additional-config.conf @@ -1,8 +1,4 @@ # Configurations here will be applied after app.conf, and before any ADDITIONAL_CONFIG env vars -# Please use this file for temporary changes that require quick iterations. Once made permanent, -# please upstream the change to the Canton repo. -canton.mediators.mediator.topology { - enable-topology-state-cache-consistency-checks = false - use-new-processor = true - use-new-client = true -} +# Please use this file for temporary changes that require quick iterations. Always add a TO-DO +# issue when adding things here and either alert an upstream team to upstream your changes +# or open PRs to upstream yourself. diff --git a/cluster/images/canton-participant/additional-config.conf b/cluster/images/canton-participant/additional-config.conf index 0921f83bf28..b5da7de50f6 100644 --- a/cluster/images/canton-participant/additional-config.conf +++ b/cluster/images/canton-participant/additional-config.conf @@ -1,10 +1,9 @@ # Configurations here will be applied after app.conf, and before any ADDITIONAL_CONFIG env vars -# Please use this file for temporary changes that require quick iterations. Once made permanent, -# please upstream the change to the Canton repo. - -# TODO(DACH-NY/canton-network-internal#4997) Remove once ACS commitment processor shutdown is quick. -canton.parameters.timeouts.processing.shutdown-processing = 10m +# Please use this file for temporary changes that require quick iterations. Always add a TO-DO +# issue when adding things here and either alert an upstream team to upstream your changes +# or open PRs to upstream yourself. +# TODO(DACH-NY/canton-network-internal#7027) Clean up once upstreamed canton.participants.participant { parameters { commitment-asynchronous-initialization = true @@ -12,9 +11,4 @@ canton.participants.participant { # which just makes things worse and overloads the node. commitment-use-db-snapshot-for-participant-lookup = true } - topology { - enable-topology-state-cache-consistency-checks = false - use-new-processor = true - use-new-client = true - } } diff --git a/cluster/images/canton-sequencer/additional-config.conf b/cluster/images/canton-sequencer/additional-config.conf index 2f651365648..e20da57caf2 100644 --- a/cluster/images/canton-sequencer/additional-config.conf +++ b/cluster/images/canton-sequencer/additional-config.conf @@ -1,13 +1,10 @@ # Configurations here will be applied after app.conf, and before any ADDITIONAL_CONFIG env vars -# Please use this file for temporary changes that require quick iterations. Once made permanent, -# please upstream the change to the Canton repo. +# Please use this file for temporary changes that require quick iterations. Always add a TO-DO +# issue when adding things here and either alert an upstream team to upstream your changes +# or open PRs to upstream yourself. +# TODO(DACH-NY/canton-network-internal#7027) Clean up once upstreamed canton.sequencers.sequencer { - topology { - enable-topology-state-cache-consistency-checks = false - use-new-processor = true - use-new-client = true - } sequencer { config.consensus-empty-block-creation-timeout = 500.milliseconds config.output-fetch-how-many-recipients = 2 diff --git a/cluster/images/cometbft-watchdog/restart-watchdog.py b/cluster/images/cometbft-watchdog/restart-watchdog.py index 34f1d09a7b5..49fd6e73dcf 100644 --- a/cluster/images/cometbft-watchdog/restart-watchdog.py +++ b/cluster/images/cometbft-watchdog/restart-watchdog.py @@ -96,7 +96,7 @@ def parse_samples(text, metric, required_labels, source): def scrape(url, metric, required_labels, timeout): try: request = urllib.request.Request( - url, headers={"Accept": "text/plain"} + f"{url}?name[]={metric}", headers={"Accept": "text/plain"} ) with urllib.request.urlopen(request, timeout=timeout) as response: charset = response.headers.get_content_charset() or "utf-8" diff --git a/cluster/images/scan-app/app.conf b/cluster/images/scan-app/app.conf index 7b5ec38a3a9..4f97b09de31 100644 --- a/cluster/images/scan-app/app.conf +++ b/cluster/images/scan-app/app.conf @@ -75,8 +75,42 @@ canton { } } rate-limiters { - getAcsSnapshot.rate-per-second = 20 - getAcsSnapshotAt.rate-per-second = 10 + getAcsSnapshot { + rate-per-second = 2 + sustained-rate-per-second = 1 + } + forceAcsSnapshotNow { + rate-per-second = 2 + sustained-rate-per-second = 1 + } + getAcsSnapshotAt { + rate-per-second = 10 + sustained-rate-per-second = 5 + } + getAcsSnapshotAtV1 { + rate-per-second = 10 + sustained-rate-per-second = 5 + } + getAcsSnapshotAtV2 { + rate-per-second = 10 + sustained-rate-per-second = 5 + } + getUpdateHistory { + rate-per-second = 50 + sustained-rate-per-second = 25 + } + getUpdateHistoryV1 { + rate-per-second = 50 + sustained-rate-per-second = 25 + } + getUpdateHistoryV2 { + rate-per-second = 50 + sustained-rate-per-second = 25 + } + getEventHistory { + rate-per-second = 50 + sustained-rate-per-second = 25 + } } } # TODO(DACH-NY/canton-network-internal#2125) Revisit timeouts on 3.4 diff --git a/cluster/images/splice-test-docker-runner/Dockerfile b/cluster/images/splice-test-docker-runner/Dockerfile index 587bfec4f2a..021d1df4412 100644 --- a/cluster/images/splice-test-docker-runner/Dockerfile +++ b/cluster/images/splice-test-docker-runner/Dockerfile @@ -1,5 +1,5 @@ -ARG RUNNER_VERSION=2.336.0 -ARG RUNNER_DIGEST=sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda +ARG RUNNER_VERSION=2.337.0 +ARG RUNNER_DIGEST=sha256:e5496277be5d09bc968b3d64911b74e219ac4a3f2edce956a3ecf9271bea1ef4 # Note that we don't currently support arm64 runners, so we build this only for amd64 FROM --platform=$BUILDPLATFORM ghcr.io/actions/actions-runner:${RUNNER_VERSION}@${RUNNER_DIGEST} diff --git a/cluster/images/splice-test-runner-hook/Dockerfile b/cluster/images/splice-test-runner-hook/Dockerfile index 7104427fb3b..e04f9a459ce 100644 --- a/cluster/images/splice-test-runner-hook/Dockerfile +++ b/cluster/images/splice-test-runner-hook/Dockerfile @@ -1,5 +1,5 @@ -ARG RUNNER_VERSION=2.336.0 -ARG RUNNER_DIGEST=sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda +ARG RUNNER_VERSION=2.337.0 +ARG RUNNER_DIGEST=sha256:e5496277be5d09bc968b3d64911b74e219ac4a3f2edce956a3ecf9271bea1ef4 # Note that we don't currently support arm64 runners, so we build this only for amd64 FROM --platform=$BUILDPLATFORM ghcr.io/actions/actions-runner:${RUNNER_VERSION}@${RUNNER_DIGEST} diff --git a/cluster/images/sv-app/app.conf b/cluster/images/sv-app/app.conf index 8b407493d49..c596d8fd6eb 100644 --- a/cluster/images/sv-app/app.conf +++ b/cluster/images/sv-app/app.conf @@ -122,8 +122,14 @@ canton { } } rate-limiters { - prepareValidatorOnboarding.rate-per-second = 1 - devNetOnboardValidatorPrepare.rate-per-second = 0.2 + prepareValidatorOnboarding { + rate-per-second = 2 + sustained-rate-per-second = 1 + } + devNetOnboardValidatorPrepare { + rate-per-second = 2 + sustained-rate-per-second = 1 + } } } } diff --git a/cluster/pulumi/canton-network/src/dso.ts b/cluster/pulumi/canton-network/src/dso.ts deleted file mode 100644 index 971708ef237..00000000000 --- a/cluster/pulumi/canton-network/src/dso.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -import * as pulumi from '@pulumi/pulumi'; -import { - Auth0Client, - CnInput, - DecentralizedSynchronizerMigrationConfig, - exactNamespace, -} from '@canton-network/splice-pulumi-common'; -import { - configForSv, - coreSvsToDeploy, - StaticSvConfig, -} from '@canton-network/splice-pulumi-common-sv'; -import { - InstalledSv, - installSvNodeStandalone, -} from '@canton-network/splice-pulumi-common-sv/src/sv'; - -interface DsoArgs { - auth0Client: Auth0Client; - decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerMigrationConfig; - exportSvResources?: boolean; -} - -export class Dso extends pulumi.ComponentResource { - args: DsoArgs; - sv1: Promise; - allSvs: Promise; - - private async installSvNode( - svConf: StaticSvConfig, - extraDependsOn: CnInput[] = [] - ): Promise { - const xns = exactNamespace(svConf.nodeName, true, this.args.exportSvResources); - const dynamicConfig = configForSv(svConf.nodeName); - const sv = await installSvNodeStandalone( - xns, - svConf, - dynamicConfig, - this.args.auth0Client, - extraDependsOn, - (this.args.exportSvResources ?? false) ? { action: 'export' } : undefined - ); - // we never run import from here so the following is ok - // migration code is temporary so it doesn't have to be beautiful - return sv!; - } - - private async installDso() { - const relevantSvConfs = coreSvsToDeploy; - const [sv1Conf, ...restSvConfs] = relevantSvConfs; - - const sv1 = await this.installSvNode(sv1Conf); - - // TODO(#893): long-term CantonBFT deployments should be robust enough to onboard in parallel again? - const incrementalOnboarding = - this.args.decentralizedSynchronizerUpgradeConfig.active.sequencer.enableBftSequencer; - - // recursive install function to allow injecting dependencies on previous svs - const installSvNodes = async ( - configs: StaticSvConfig[], - previousSvs: InstalledSv[] = [] - ): Promise => { - if (configs.length === 0) { - return previousSvs; - } - const [conf, ...remainingConfigs] = configs; - - const newSv = await this.installSvNode( - conf, - incrementalOnboarding ? previousSvs.map(sv => sv.svApp) : [] - ); - return installSvNodes(remainingConfigs, [...previousSvs, newSv]); - }; - const restSvs = await installSvNodes(restSvConfs); - - return { sv1, allSvs: [sv1, ...restSvs] }; - } - - constructor(name: string, args: DsoArgs, opts?: pulumi.ComponentResourceOptions) { - super('canton:network:dso', name, args, opts); - this.args = args; - - const dso = this.installDso(); - - this.sv1 = dso.then(r => r.sv1); - - this.allSvs = dso.then(r => r.allSvs); - - this.registerOutputs({}); - } -} diff --git a/cluster/pulumi/canton-network/src/index.ts b/cluster/pulumi/canton-network/src/index.ts index f87e5151b20..9ba4fb095a7 100644 --- a/cluster/pulumi/canton-network/src/index.ts +++ b/cluster/pulumi/canton-network/src/index.ts @@ -12,29 +12,20 @@ async function auth0CacheAndInstallCluster(auth0Fetch: Auth0Fetch) { installClusterVersion(); - const dso = await installCluster(auth0Fetch); + await installCluster(auth0Fetch); await auth0Fetch.saveAuth0Cache(); - - return (await dso?.allSvs)?.map(sv => ({ - nodeName: sv.nodeName, - databaseInstanceName: sv.appsPostgres.databaseId, - databaseSecretName: sv.appsPostgres.secretName, - })); } async function main() { const auth0FetchOutput = getAuth0Config(Auth0ClientType.MAINSTACK); - const svs = auth0FetchOutput.apply(async auth0Fetch => { - const svs = await auth0CacheAndInstallCluster(auth0Fetch); + auth0FetchOutput.apply(async auth0Fetch => { + await auth0CacheAndInstallCluster(auth0Fetch); scheduleLoadGenerator(auth0Fetch, []); - - return svs; }); - - return svs; } -export const svs = pulumi.output(main()); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +main(); diff --git a/cluster/pulumi/canton-network/src/installCluster.ts b/cluster/pulumi/canton-network/src/installCluster.ts index a856f1fd7fa..3e13cef35b9 100644 --- a/cluster/pulumi/canton-network/src/installCluster.ts +++ b/cluster/pulumi/canton-network/src/installCluster.ts @@ -3,7 +3,6 @@ import { Auth0Client, config, - DecentralizedSynchronizerUpgradeConfig, exactNamespace, isDevNet, spliceConfig, @@ -13,13 +12,10 @@ import { configureScanBigQuery, ScanBigQueryArgs, } from '@canton-network/splice-pulumi-common-sv/src/bigQuery'; -import { InstalledSv } from '@canton-network/splice-pulumi-common-sv/src/sv'; -import { CloudPostgres } from '@canton-network/splice-pulumi-common/src/postgres'; import { activeVersion } from '../../common'; import { installChaosMesh } from './chaosMesh'; import { installDocs } from './docs'; -import { Dso } from './dso'; /// Toplevel Chart Installs @@ -27,25 +23,14 @@ console.error(`Launching with isDevNet: ${isDevNet}`); const enableChaosMesh = config.envFlag('ENABLE_CHAOS_MESH'); -export async function installCluster(auth0Client: Auth0Client): Promise { +export async function installCluster(auth0Client: Auth0Client): Promise { console.error( activeVersion.type === 'local' ? 'Using locally built charts by default' : `Using charts from the container registry by default, version ${activeVersion.version}` ); - // TODO(#6719) once all clusters have been migrated this can be removed. - const dso = spliceConfig.configuration.synchronizerMigration.splitSvDeploymentEnabled - ? undefined - : new Dso('dso', { - auth0Client, - decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerUpgradeConfig, - exportSvResources: - spliceConfig.configuration.synchronizerMigration.migrateToSplitSvDeployment, - }); - - const locallyInstalledSvs = await dso?.allSvs; - const bigQueryArgs = [...iterateBigQueryArgs(locallyInstalledSvs)]; + const bigQueryArgs = [...iterateBigQueryArgs()]; if (bigQueryArgs.length > 1) { throw new Error( `Multiple SVs with BigQuery configuration found: ${bigQueryArgs.map(arg => arg.namespace.logicalName).join(', ')}` @@ -55,59 +40,29 @@ export async function installCluster(auth0Client: Auth0Client): Promise [ - sv.scan, - sv.svApp, - sv.validatorApp, - sv.ingress, - ]); - installDocs(); if (enableChaosMesh) { - installChaosMesh({ dependsOn: svDependencies }); + installChaosMesh({ dependsOn: [] }); } - - return dso; } -function* iterateBigQueryArgs( - locallyInstalledSvs?: Array -): Generator { - if (locallyInstalledSvs === undefined) { - for (const sv of coreSvsToDeploy) { - const config = configForSv(sv.nodeName); - const bigQueryConfig = config?.scanApp?.bigQuery; - const cloudSqlEnabled = (config.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql) - .enabled; - if (bigQueryConfig !== undefined && cloudSqlEnabled) { - const namespace = exactNamespace(sv.nodeName, true, true); - yield { - namespace, - bigQueryConfig: bigQueryConfig, - scanReference: { - type: 'external', - databaseInstanceNamePrefix: `${namespace.logicalName}-cn-apps-pg`, - }, - }; - } - } - } else { - // TODO(#6719) once all clusters have been migrated this can be removed. - for (const sv of locallyInstalledSvs) { - const config = configForSv(sv.nodeName); - const bigQueryConfig = config?.scanApp?.bigQuery; - if (bigQueryConfig !== undefined && sv.appsPostgres instanceof CloudPostgres) { - yield { - namespace: sv.namespace, - bigQueryConfig: bigQueryConfig, - scanReference: { - type: 'local', - databaseInstance: sv.appsPostgres.databaseInstance, - chart: sv.scan, - }, - }; - } +function* iterateBigQueryArgs(): Generator { + for (const sv of coreSvsToDeploy) { + const config = configForSv(sv.nodeName); + const bigQueryConfig = config?.scanApp?.bigQuery; + const cloudSqlEnabled = (config.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql) + .enabled; + if (bigQueryConfig !== undefined && cloudSqlEnabled) { + const namespace = exactNamespace(sv.nodeName, true, true); + yield { + namespace, + bigQueryConfig: bigQueryConfig, + scanReference: { + type: 'external', + databaseInstanceNamePrefix: `${namespace.logicalName}-cn-apps-pg`, + }, + }; } } } diff --git a/cluster/pulumi/common-sv/src/config.ts b/cluster/pulumi/common-sv/src/config.ts index 38a083bc760..5d4ada44965 100644 --- a/cluster/pulumi/common-sv/src/config.ts +++ b/cluster/pulumi/common-sv/src/config.ts @@ -130,6 +130,11 @@ export const SvConfigSchema = z.object({ skipInitialization: z.boolean().default(false), // This can be used on clusters like CILR where we usually would expect to skip initialization but the sv runbook gets reset periodically. forceSvRunbookInitialization: z.boolean().default(false), + sequencer: z + .object({ + externalRateLimits: RateLimitSchema, + }) + .optional(), }) .optional(), }) diff --git a/cluster/pulumi/common-sv/src/loopback.ts b/cluster/pulumi/common-sv/src/loopback.ts index 1d2c1b2c6cf..c850a10a984 100644 --- a/cluster/pulumi/common-sv/src/loopback.ts +++ b/cluster/pulumi/common-sv/src/loopback.ts @@ -11,7 +11,7 @@ import { ExactNamespace, } from '@canton-network/splice-pulumi-common'; -import { allSvsToDeploy, coreSvsToDeploy } from './svConfigs'; +import { coreSvsToDeploy, svRunbookConfig } from './svConfigs'; import { cometBFTExternalPort } from './synchronizer/cometbftConfig'; export function installSvLoopback(namespace: ExactNamespace, cometbft: boolean): pulumi.Resource[] { @@ -66,7 +66,9 @@ export function installLoopback( { dependsOn: [namespace.ns] } ); - const svHosts = allSvsToDeploy + // always excplitly include the runbook to avoid issues where the loopback will not work + const svHosts = coreSvsToDeploy + .concat([svRunbookConfig]) .map(sv => [`${sv.ingressName}.${clusterHostname}`, `*.${sv.ingressName}.${clusterHostname}`]) .flat(); const allHosts = [ diff --git a/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts b/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts index e885d75dd69..28a8fdf77d2 100644 --- a/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts +++ b/cluster/pulumi/common-sv/src/physicalSynchronizerConfig.ts @@ -21,7 +21,7 @@ export const SvMediatorConfigSchema = z cloudSql: CloudSqlWithOverrideConfigSchema, // Mediator is either deployed on cloudSQL or is reset frequently, so we can skip migration splicePostgres: SplicePostgresSchema.default({ - postgresImage: 'postgres:18', + postgresImage: 'postgres:18-trixie', deployment: 'docker-image', }), resources: K8sResourceSchema, @@ -35,7 +35,7 @@ export const SvSequencerConfigSchema = z cloudSql: CloudSqlWithOverrideConfigSchema, // Sequencer is either deployed on cloudSQL or is reset frequently, so we can skip migration splicePostgres: SplicePostgresSchema.default({ - postgresImage: 'postgres:18', + postgresImage: 'postgres:18-trixie', deployment: 'docker-image', }), resources: K8sResourceSchema, diff --git a/cluster/pulumi/common-sv/src/sv.ts b/cluster/pulumi/common-sv/src/sv.ts deleted file mode 100644 index 1d373a98f9e..00000000000 --- a/cluster/pulumi/common-sv/src/sv.ts +++ /dev/null @@ -1,833 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -import * as postgres from '@canton-network/splice-pulumi-common/src/postgres'; -import * as k8s from '@pulumi/kubernetes'; -import * as pulumi from '@pulumi/pulumi'; -import { - activeVersion, - ansDomainPrefix, - appsKubernetesScheduling, - Auth0Client, - btoa, - ChartValues, - CLUSTER_BASENAME, - CLUSTER_HOSTNAME, - CnInput, - config as envConfig, - daContactPoint, - DecentralizedSynchronizerMigrationConfig, - DecentralizedSynchronizerUpgradeConfig, - ExactNamespace, - envoyClientIpHeaderEnvVar, - failOnAppVersionMismatch, - fetchAndInstallParticipantBootstrapDump, - getAdditionalJvmOptions, - getSvAppApiAudience, - imagePullSecret, - initialPackageConfigJson, - initialSynchronizerFeesConfig, - InstalledHelmChart, - installSpliceHelmChart, - installSvAppSecrets, - installValidatorOnboardingSecret, - isDevNet, - networkWideConfig, - participantBootstrapDumpSecretName, - PersistenceConfig, - persistentHeapDumpsPvc, - sanitizedForPostgres, - spliceInstanceNames, - SplicePostgresConfig, - svCometBftGovernanceKeyFromSecret, - svCometBftGovernanceKeySecret, - SvIdKey, - svKeyFromSecret, - svOnboardingPollingInterval, - svValidatorTopupConfig, - svUserIds, - validatorOnboardingSecretName, -} from '@canton-network/splice-pulumi-common'; -import { - approvedSvIdentities, - CantonBftSynchronizerNode, - configForSv, - coreSvsToDeploy, - DecentralizedSynchronizerNode, - initialRound, - installScanBulkStorage, - installSvLoopback, - SingleSvConfiguration, - StaticSvConfig, - SynchronizerNodes, - valuesForSvApp, - valuesForSvValidatorApp, -} from '@canton-network/splice-pulumi-common-sv'; -import { SvConfig, svsConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; -import { readBackupConfig } from '@canton-network/splice-pulumi-common-validator/src/backup'; -import { installValidatorApp } from '@canton-network/splice-pulumi-common-validator/src/validator'; -import { - mustInstallSplitwell, - mustInstallValidator1, - splitwellOnboarding, - standaloneValidatorOnboarding, - validator1Onboarding, -} from '@canton-network/splice-pulumi-common-validator/src/validators'; -import { - delegatelessAutomationExpectedTaskDuration, - delegatelessAutomationExpiredRewardCouponBatchSize, - delegatelessAutomationExpiredRewardCouponNumBatches, -} from '@canton-network/splice-pulumi-common/src/automation'; -import { - BucketConfig, - installBucketSecret, -} from '@canton-network/splice-pulumi-common/src/buckets'; -import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; -import { SplitPostgresInstances } from '@canton-network/splice-pulumi-common/src/config/configs'; -import { initialAmuletPrice } from '@canton-network/splice-pulumi-common/src/initialAmuletPrice'; -import { Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; -import { installRateLimits } from '@canton-network/splice-pulumi-common/src/ratelimit/rateLimit'; -import { topologySnapshotConfig } from '@canton-network/splice-pulumi-common/src/topology-snapshot'; -import { Resource } from '@pulumi/pulumi'; -import pick from 'lodash/pick'; - -import { installInfo } from './info'; - -// TODO(#6719) once all clusters have been migrated move the whole module to the sv project -export async function installSvNodeStandalone( - xns: ExactNamespace, - staticConfig: StaticSvConfig, - config: SingleSvConfiguration, - auth0Client: Auth0Client, - extraDependsOn: CnInput[] = [], - // TODO(#6719) once all clusters have been migrated remove this - migrationArgs?: MigrationArgs -): Promise { - const nodeName = staticConfig.nodeName; - const [sv1StaticConfig, ...otherSvsStaticConfigs] = coreSvsToDeploy; - const isFoundingSv = nodeName === sv1StaticConfig.nodeName; - const disableOnboardingParticipantPromotionDelay = envConfig.envFlag( - 'DISABLE_ONBOARDING_PARTICIPANT_PROMOTION_DELAY', - false - ); - return await installSvNode( - xns, - { - isFirstSv: isFoundingSv, - ...pick(staticConfig, [ - 'nodeName', - 'ingressName', - 'onboardingName', - 'cometBft', - 'validatorWalletUser', - 'auth0ValidatorAppName', - 'auth0SvAppName', - 'sweep', - ]), - nodeConfigs: { - sv1: { - ...sv1StaticConfig.cometBft, - ...pick(sv1StaticConfig, ['nodeName', 'ingressName']), - }, - peers: otherSvsStaticConfigs - .filter(config => config.nodeName !== nodeName) - .map(config => ({ - ...config.cometBft, - ...pick(config, ['nodeName', 'ingressName']), - })), - }, - onboarding: isFoundingSv - ? { - type: 'found-dso', - sv1SvRewardWeightBps: - approvedSvIdentities().find( - identity => identity.name == sv1StaticConfig.onboardingName - )?.rewardWeightBps ?? 10_000, - roundZeroDuration: envConfig.optionalEnv('ROUND_ZERO_DURATION'), - initialRound: initialRound?.toString(), - } - : { - type: 'join-with-key', - sponsorApiUrl: `http://sv-app.sv-1:5014`, - sponsorScanUrl: `http://scan-app.sv-1:5012`, - keys: svKeyFromSecret( - staticConfig.svIdKeySecretName ?? `${nodeName.replaceAll('-', '')}-id` - ), - }, - auth0Client, - expectedValidatorOnboardings: isFoundingSv - ? [ - ...(function* () { - if (mustInstallSplitwell) { - yield splitwellOnboarding; - } - if (mustInstallValidator1) { - yield validator1Onboarding; - } - if (standaloneValidatorOnboarding !== undefined) { - yield standaloneValidatorOnboarding; - } - })(), - ] - : [], - isDevNet, - ...(await readBackupConfig()), - topupConfig: svValidatorTopupConfig, - splitPostgresInstances: SplitPostgresInstances, - disableOnboardingParticipantPromotionDelay, - onboardingPollingInterval: svOnboardingPollingInterval, - cometBftGovernanceKey: - config.participant?.kms !== undefined - ? svCometBftGovernanceKeyFromSecret( - staticConfig.cometBftGovernanceKeySecretName ?? - `${nodeName.replaceAll('-', '')}-cometbft-governance-key` - ) - : undefined, - initialRound: initialRound?.toString(), - version: config.versionOverride ?? activeVersion, - ...config, - }, - DecentralizedSynchronizerUpgradeConfig, - extraDependsOn, - migrationArgs - ); -} - -export function installSvKeySecret( - xns: ExactNamespace, - keys: CnInput -): k8s.core.v1.Secret[] { - const legacySecretName = 'cn-app-sv-key'; - const secretName = 'splice-app-sv-key'; - - const data = pulumi.output(keys).apply(ks => { - return { - public: btoa(ks.publicKey), - private: btoa(ks.privateKey), - }; - }); - - return [ - new k8s.core.v1.Secret( - `cn-app-${xns.logicalName}-key`, - { - metadata: { - name: legacySecretName, - namespace: xns.logicalName, - }, - type: 'Opaque', - data: data, - }, - { - dependsOn: [xns.ns], - } - ), - new k8s.core.v1.Secret( - `splice-app-${xns.logicalName}-key`, - { - metadata: { - name: secretName, - namespace: xns.logicalName, - }, - type: 'Opaque', - data: data, - }, - { - dependsOn: [xns.ns], - } - ), - ]; -} - -export type InstalledSv = { - namespace: ExactNamespace; - nodeName: string; - validatorApp: Resource; - svApp: InstalledHelmChart; - scan: InstalledHelmChart; - canton: SynchronizerNodes; - ingress: Resource; - appsPostgres: postgres.Postgres; -}; - -export async function installSvNode( - xns: ExactNamespace, - baseConfig: SvConfig, - decentralizedSynchronizerUpgradeConfig: DecentralizedSynchronizerMigrationConfig, - extraDependsOn: CnInput[] = [], - migrationArgs?: MigrationArgs -): Promise { - const periodicBackupConfig: BucketConfig | undefined = baseConfig.periodicBackupConfig - ? { - ...baseConfig.periodicBackupConfig, - location: { - ...baseConfig.periodicBackupConfig.location, - prefix: - baseConfig.periodicBackupConfig.location.prefix || - `${CLUSTER_BASENAME}/${xns.logicalName}`, - }, - } - : undefined; - - const svConfig = configForSv(baseConfig.nodeName); - const periodicTopologySnapshotConfig = svConfig.periodicSnapshots?.topology - ? await topologySnapshotConfig( - svConfig.periodicSnapshots?.topology, - `${CLUSTER_BASENAME}/${xns.logicalName}` - ) - : undefined; - - const identitiesBackupLocation = { - ...baseConfig.identitiesBackupLocation, - prefix: baseConfig.identitiesBackupLocation.prefix || `${CLUSTER_BASENAME}/${xns.logicalName}`, - }; - - const bulkStorageBuckets = svConfig.scanApp?.bulkStorage - ? installScanBulkStorage(xns, svConfig.scanApp.bulkStorage) - : undefined; - - const config = { - ...baseConfig, - periodicBackupConfig, - identitiesBackupLocation, - bulkStorageBuckets, - }; - - if (migrationArgs?.action === 'import') { - const appsPostgres = await installAppsPostgres(xns, config, migrationArgs); - - return undefined; - } else { - const loopback = installSvLoopback(xns, decentralizedSynchronizerUpgradeConfig.usesCometbft()); - const imagePullDeps = imagePullSecret(xns); - - const auth0Secrets: CnInput[] = await installSvAppSecrets( - xns, - baseConfig.auth0Client - ); - - const identitiesBackupConfigSecret = installBucketSecret( - xns, - config.identitiesBackupLocation.bucket - ); - - const topologySnapshotConfigSecret = periodicTopologySnapshotConfig - ? installBucketSecret(xns, periodicTopologySnapshotConfig.location.bucket) - : undefined; - const backupConfigSecret: pulumi.Resource | undefined = config.periodicBackupConfig - ? config.periodicBackupConfig.location.bucket != config.identitiesBackupLocation.bucket - ? installBucketSecret(xns, config.periodicBackupConfig.location.bucket) - : identitiesBackupConfigSecret - : undefined; - - const participantBootstrapDumpSecret: pulumi.Resource | undefined = - config.bootstrappingDumpConfig - ? await fetchAndInstallParticipantBootstrapDump(xns, config.bootstrappingDumpConfig) - : undefined; - - const dependsOn: CnInput[] = auth0Secrets - .concat( - config.onboarding.type == 'join-with-key' - ? installSvKeySecret(xns, config.onboarding.keys) - : [] - ) - .concat( - config.onboarding.type == 'join-with-key' && - config.onboarding.sponsorRelease !== undefined && - spliceConfig.pulumiProjectConfig.interAppsDependencies - ? [config.onboarding.sponsorRelease] - : [] - ) - .concat( - config.expectedValidatorOnboardings.map(onboarding => - installValidatorOnboardingSecret(xns, onboarding.name, onboarding.secret) - ) - ) - .concat([identitiesBackupConfigSecret]) - .concat(backupConfigSecret ? [backupConfigSecret] : []) - .concat(topologySnapshotConfigSecret ? [topologySnapshotConfigSecret] : []) - .concat(participantBootstrapDumpSecret ? [participantBootstrapDumpSecret] : []) - .concat(loopback) - .concat(imagePullDeps) - .concat( - config.cometBftGovernanceKey - ? svCometBftGovernanceKeySecret(xns, config.cometBftGovernanceKey) - : [] - ) - .concat( - bulkStorageBuckets - ? [ - bulkStorageBuckets.staging.secret, - bulkStorageBuckets.staging.bucket, - bulkStorageBuckets.committed.secret, - bulkStorageBuckets.committed.bucket, - ] - : [] - ) - .concat(extraDependsOn); - - const appsPostgres = await installAppsPostgres(xns, config, migrationArgs); - - const canton = new SynchronizerNodes( - decentralizedSynchronizerUpgradeConfig, - { - ...config.nodeConfigs, - self: { ...config.cometBft, nodeName: config.nodeName }, - }, - config.ingressName - ); - - const svApp = installSvApp( - decentralizedSynchronizerUpgradeConfig, - { ...config, periodicTopologySnapshotConfig }, - xns, - dependsOn, - appsPostgres, - canton - ); - - const scan = installScan( - xns, - config, - decentralizedSynchronizerUpgradeConfig, - dependsOn, - canton, - svApp, - appsPostgres - ); - - installInfo( - xns, - `info.${config.ingressName}.${CLUSTER_HOSTNAME}`, - 'cluster-ingress/cn-http-gateway', - decentralizedSynchronizerUpgradeConfig, - `http://scan-app.${config.nodeName}:5012`, - scan, - config.version - ); - - const validatorApp = await installValidator( - appsPostgres, - xns, - decentralizedSynchronizerUpgradeConfig, - baseConfig, - backupConfigSecret, - canton, - svApp, - scan - ); - - const ingress = installSpliceHelmChart( - xns, - 'ingress-sv', - 'splice-cluster-ingress-runbook', - { - withSvIngress: true, - ingress: { - decentralizedSynchronizer: { - migrationIds: decentralizedSynchronizerUpgradeConfig - .runningMigrations() - .map(x => x.id.toString()), - }, - }, - spliceDomainNames: { - nameServiceDomain: ansDomainPrefix, - }, - cluster: { - hostname: CLUSTER_HOSTNAME, - svNamespace: xns.logicalName, - svIngressName: config.ingressName, - }, - rateLimit: { - scan: { - enable: false, - }, - }, - }, - config.version, - { dependsOn: [xns.ns] } - ); - - return { - namespace: xns, - nodeName: config.nodeName, - canton, - validatorApp, - svApp, - scan, - ingress, - appsPostgres, - }; - } -} - -async function installAppsPostgres( - xns: ExactNamespace, - config: SvConfig, - migrationArgs?: MigrationArgs -): Promise { - const defaultPostgres = config.splitPostgresInstances - ? undefined - : await postgres.installPostgres( - xns, - 'postgres', - 'postgres', - config.version, - spliceConfig.pulumiProjectConfig.cloudSql, - spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, - false, - { - logicalDecoding: !!config.scanApp?.bigQuery, - } - ); - - const appsPostgres = - defaultPostgres || - (await postgres.installPostgres( - xns, - `cn-apps-pg`, - `cn-apps-pg`, - config.version, - config.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql, - spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, - true, - { - logicalDecoding: !!config.scanApp?.bigQuery, - ...(() => { - switch (migrationArgs?.action) { - case 'import': - return { - existingInstanceName: migrationArgs.databaseInstanceName, - existingSecretName: migrationArgs.databaseSecretName, - }; - case 'export': - return { - retainDbResourcesOnDelete: true, - }; - case undefined: - return {}; - } - })(), - } - )); - - return appsPostgres; -} - -function persistenceConfig(postgresDb: postgres.Postgres, dbName: string): PersistenceConfig { - const dbNameO = pulumi.Output.create(dbName); - return { - host: postgresDb.address, - databaseName: dbNameO, - secretName: postgresDb.secretName, - schema: dbNameO, - user: pulumi.Output.create('cnadmin'), - port: pulumi.Output.create(5432), - postgresName: postgresDb.instanceName, - }; -} - -async function installValidator( - postgres: Postgres, - xns: ExactNamespace, - decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig, - svConfig: SvConfig, - backupConfigSecret: Resource | undefined, - sv: SynchronizerNodes, - svApp: Resource, - scan: Resource -) { - const validatorDbName = `validator_${sanitizedForPostgres(svConfig.nodeName)}`; - const commonValidatorAppValues = valuesForSvValidatorApp( - decentralizedSynchronizerMigrationConfig, - svConfig - ); - - return await installValidatorApp({ - xns, - ...commonValidatorAppValues, - validatorWalletUsers: svUserIds(svConfig.auth0Client.getCfg()).apply(ids => - ids.concat(svConfig.validatorWalletUser ? [svConfig.validatorWalletUser] : []) - ), - dependencies: [], - disableAllocateLedgerApiUserParty: true, - topupConfig: svConfig.topupConfig, - backupConfig: - svConfig.periodicBackupConfig && backupConfigSecret - ? { - config: svConfig.periodicBackupConfig, - secret: backupConfigSecret, - } - : undefined, - persistenceConfig: persistenceConfig(postgres, validatorDbName), - extraDependsOn: spliceConfig.pulumiProjectConfig.interAppsDependencies - ? [svApp, postgres, scan] - : [postgres], - svValidator: true, - participantAddress: sv.participant.internalClusterAddress, - scanAddress: internalScanUrl(svConfig), - auth0Client: svConfig.auth0Client, - auth0ValidatorAppName: svConfig.auth0ValidatorAppName, - sweep: svConfig.sweep, - nodeIdentifier: svConfig.onboardingName, - logLevel: svConfig.logging?.appsLogLevel, - logAsync: svConfig.logging?.appsAsync, - apiRequestLogLevel: svConfig.logging?.apiRequestLogLevel, - additionalJvmOptions: svConfig.validatorApp?.additionalJvmOptions || '', - resources: svConfig.validatorApp?.resources, - version: svConfig.version, - }); -} - -function publicScanUrl(config: SvConfig) { - return `https://scan.${config.ingressName}.${CLUSTER_HOSTNAME}`; -} - -function internalScanUrl(config: SvConfig): string { - return `http://scan-app.${config.nodeName}:5012`; -} - -function installSvApp( - decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig, - config: SvConfig, - xns: ExactNamespace, - dependsOn: CnInput[], - postgres: Postgres, - synchronizerNodes: SynchronizerNodes -) { - const { participant } = synchronizerNodes; - const decentralizedSynchronizer = synchronizerNodes.active; - const allSynchronizerDependencies = [ - synchronizerNodes.active, - synchronizerNodes.legacy, - synchronizerNodes.upgrade, - ...synchronizerNodes.additionalLegacy, - ] - .filter((n): n is NonNullable => n !== undefined) - .flatMap(n => n.dependencies); - const svDbName = `sv_${sanitizedForPostgres(config.nodeName)}`; - const commonSvAppValues = valuesForSvApp( - decentralizedSynchronizerMigrationConfig, - { ...config, skipInitialization: svsConfig?.synchronizer?.skipInitialization }, - synchronizerNodes, - config.ingressName - ); - - const svValues = { - ...commonSvAppValues, - ...spliceInstanceNames, - onboardingType: config.onboarding.type, - onboardingName: config.onboardingName, - onboardingFoundingSvRewardWeightBps: - config.onboarding.type == 'found-dso' ? config.onboarding.sv1SvRewardWeightBps : undefined, - onboardingRoundZeroDuration: - config.onboarding.type == 'found-dso' ? config.onboarding.roundZeroDuration : undefined, - initialSynchronizerFeesConfig: - config.onboarding.type == 'found-dso' ? initialSynchronizerFeesConfig : undefined, - initialPackageConfigJson: - config.onboarding.type == 'found-dso' ? initialPackageConfigJson : undefined, - initialRound: - config.onboarding.type == 'found-dso' ? config.onboarding.initialRound : undefined, - initialAmuletPrice: initialAmuletPrice, - disableOnboardingParticipantPromotionDelay: config.disableOnboardingParticipantPromotionDelay, - decentralizedSynchronizerUrl: - config.onboarding.type == 'found-dso' - ? undefined - : decentralizedSynchronizer.sv1InternalSequencerAddress, - scan: { - publicUrl: publicScanUrl(config), - internalUrl: internalScanUrl(config), - }, - expectedValidatorOnboardings: config.expectedValidatorOnboardings.map(onboarding => ({ - expiresIn: onboarding.expiresIn, - secretFrom: { - secretKeyRef: { - name: validatorOnboardingSecretName(onboarding.name), - key: 'secret', - optional: false, - }, - }, - })), - isDevNet: config.isDevNet, - approvedSvIdentities: approvedSvIdentities(), - persistence: persistenceConfig(postgres, svDbName), - identitiesExport: config.identitiesBackupLocation, - participantIdentitiesDumpImport: config.bootstrappingDumpConfig - ? { secretName: participantBootstrapDumpSecretName } - : undefined, - metrics: { - enable: true, - }, - additionalJvmOptions: getAdditionalJvmOptions(config.svApp?.additionalJvmOptions), - failOnAppVersionMismatch: failOnAppVersionMismatch, - participantAddress: participant.internalClusterAddress, - onboardingPollingInterval: config.onboardingPollingInterval, - enablePostgresMetrics: true, - auth: { - audience: getSvAppApiAudience(config.auth0Client.getCfg(), xns.logicalName), - jwksUrl: `https://${config.auth0Client.getCfg().auth0Domain}/.well-known/jwks.json`, - }, - contactPoint: daContactPoint, - nodeIdentifier: config.onboardingName, - delegatelessAutomationExpectedTaskDuration: delegatelessAutomationExpectedTaskDuration, - delegatelessAutomationExpiredRewardCouponBatchSize: - delegatelessAutomationExpiredRewardCouponBatchSize, - delegatelessAutomationExpiredRewardCouponNumBatches: - delegatelessAutomationExpiredRewardCouponNumBatches, - maxVettingDelay: networkWideConfig?.maxVettingDelay, - logLevel: config.logging?.appsLogLevel, - apiRequestLogLevel: config.logging?.apiRequestLogLevel, - logAsyncFlush: config.logging?.appsAsync, - resources: config.svApp?.resources, - periodicTopologySnapshotConfig: config.periodicTopologySnapshotConfig, - persistentDataPvc: persistentHeapDumpsPvc(), - } as ChartValues; - - if (config.onboarding.type == 'join-with-key') { - svValues.joinWithKeyOnboarding = { - sponsorApiUrl: config.onboarding.sponsorApiUrl, - sponsorScanUrl: config.onboarding.sponsorScanUrl, - }; - } - - return installSpliceHelmChart( - xns, - 'sv-app', - 'splice-sv-node', - svValues, - config.version, - { - dependsOn: dependsOn.concat([postgres]).concat(allSynchronizerDependencies), - }, - undefined, - appsKubernetesScheduling - ); -} - -function installScan( - xns: ExactNamespace, - config: SvConfig, - decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig, - dependsOn: CnInput[], - synchronizerNodes: SynchronizerNodes, - svApp: pulumi.Resource, - postgres: Postgres -) { - const useCantonBft = decentralizedSynchronizerMigrationConfig.active.sequencer.enableBftSequencer; - const { active, participant } = synchronizerNodes; - const scanDbName = `scan_${sanitizedForPostgres(config.nodeName)}`; - - const cantonBftConfigFor = (node: DecentralizedSynchronizerNode) => { - return { - cantonBft: { - p2pUrl: (node as unknown as CantonBftSynchronizerNode).externalSequencerP2pAddress, - }, - }; - }; - - const synchronizerValues = { - synchronizers: { - current: { - sequencer: active.namespaceInternalSequencerAddress, - mediator: active.namespaceInternalMediatorAddress, - ...(useCantonBft ? cantonBftConfigFor(active) : {}), - }, - ...(synchronizerNodes.upgrade - ? { - successor: { - sequencer: synchronizerNodes.upgrade.namespaceInternalSequencerAddress, - mediator: synchronizerNodes.upgrade.namespaceInternalMediatorAddress, - ...(decentralizedSynchronizerMigrationConfig.upgrade?.sequencer.enableBftSequencer - ? cantonBftConfigFor(synchronizerNodes.upgrade) - : {}), - }, - } - : {}), - ...(synchronizerNodes.legacy - ? { - legacy: { - sequencer: synchronizerNodes.legacy.namespaceInternalSequencerAddress, - mediator: synchronizerNodes.legacy.namespaceInternalMediatorAddress, - ...(decentralizedSynchronizerMigrationConfig.legacy?.sequencer.enableBftSequencer - ? cantonBftConfigFor(synchronizerNodes.legacy) - : {}), - }, - } - : {}), - }, - }; - - const scanValues = { - ...spliceInstanceNames, - metrics: { - enable: true, - }, - isFirstSv: config.isFirstSv, - persistence: persistenceConfig(postgres, scanDbName), - additionalJvmOptions: getAdditionalJvmOptions(config.scanApp?.additionalJvmOptions), - failOnAppVersionMismatch: failOnAppVersionMismatch, - participantAddress: participant.internalClusterAddress, - ...(config.onboarding.type == 'join-with-key' - ? { sponsorScanUrl: config.onboarding.sponsorScanUrl } - : {}), - ...synchronizerValues, - enablePostgresMetrics: true, - logLevel: config.logging?.appsLogLevel, - apiRequestLogLevel: config.logging?.apiRequestLogLevel, - logAsyncFlush: config.logging?.appsAsync, - additionalEnvVars: (config.scanApp?.additionalEnvVars || []).concat([ - envoyClientIpHeaderEnvVar('canton.scan-apps.scan-app'), - ]), - resources: config.scanApp?.resources, - ...(config.bulkStorageBuckets - ? { - bulkStorage: { - staging: { - region: config.bulkStorageBuckets.staging.region, - bucketName: config.bulkStorageBuckets.staging.bucket.name, - endpoint: 'https://storage.googleapis.com', // gcs endpoint for s3 - secretName: config.bulkStorageBuckets.staging.secret.metadata.name, - }, - committed: { - region: config.bulkStorageBuckets.committed.region, - bucketName: config.bulkStorageBuckets.committed.bucket.name, - endpoint: 'https://storage.googleapis.com', // gcs endpoint for s3 - secretName: config.bulkStorageBuckets.committed.secret.metadata.name, - }, - }, - } - : {}), - publicUrl: publicScanUrl(config), - pvc: persistentHeapDumpsPvc(), - }; - - if (svsConfig?.scan?.externalRateLimits) { - installRateLimits(xns.logicalName, 'scan-app', 5012, svsConfig.scan.externalRateLimits); - } - - return installSpliceHelmChart(xns, 'scan', 'splice-scan', scanValues, config.version, { - // TODO(#893) if possible, don't require parallel start of sv app and scan when using CantonBft - dependsOn: dependsOn - .concat(active.dependencies) - .concat( - spliceConfig.pulumiProjectConfig.interAppsDependencies && !useCantonBft - ? [svApp] - : [postgres] - ), - }); -} - -export type MigrationArgs = - | { - action: 'import'; - databaseInstanceName: string; - databaseSecretName: string; - } - | { - action: 'export'; - }; - -export type SvsMigrationOutput = Array<{ - nodeName: string; - databaseInstanceName: string; - databaseSecretName: string; -}>; diff --git a/cluster/pulumi/common/src/config/configSchema.ts b/cluster/pulumi/common/src/config/configSchema.ts index 3e6fec7028e..5e159d2de7d 100644 --- a/cluster/pulumi/common/src/config/configSchema.ts +++ b/cluster/pulumi/common/src/config/configSchema.ts @@ -15,7 +15,7 @@ const PulumiProjectConfigSchema = z.object({ cloudSql: CloudSqlConfigSchema, defaultSplicePostgresConfig: SplicePostgresSchema.default({ deployment: 'docker-image', - postgresImage: 'postgres:18', + postgresImage: 'postgres:18-trixie', }), allowDowngrade: z.boolean(), replacePostgresStatefulSetOnChanges: z.boolean().default(false), diff --git a/cluster/pulumi/common/src/config/migrationSchema.ts b/cluster/pulumi/common/src/config/migrationSchema.ts index e159021dd19..879475b52f4 100644 --- a/cluster/pulumi/common/src/config/migrationSchema.ts +++ b/cluster/pulumi/common/src/config/migrationSchema.ts @@ -60,8 +60,5 @@ export const SynchronizerMigrationSchema = z activeDatabaseId: z.number().optional(), attachPvc: z.boolean().default(true), frozenMigrationId: z.number(), - // TODO(#6719) once all clusters have been migrated the following two flags can be removed, hardcoding splitSvDeploymentEnabled to true. - splitSvDeploymentEnabled: z.boolean().default(false), - migrateToSplitSvDeployment: z.boolean().default(false), }) .strict(); diff --git a/cluster/pulumi/common/src/config/scanEndpoints.ts b/cluster/pulumi/common/src/config/scanEndpoints.ts deleted file mode 100644 index 79e63b4e567..00000000000 --- a/cluster/pulumi/common/src/config/scanEndpoints.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -import * as path from 'path'; -import { z } from 'zod'; - -import { readAndParseYaml } from './configLoader'; - -const scanYamlPath = path.join(__dirname, '../../../../../apps/scan/src/main/openapi/scan.yaml'); -const tokenRegistryYamlPaths = [ - // V1 Specs - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-metadata-v1/openapi/token-metadata-v1.yaml' - ), - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-allocation-v1/openapi/allocation-v1.yaml' - ), - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-allocation-instruction-v1/openapi/allocation-instruction-v1.yaml' - ), - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-transfer-instruction-v1/openapi/transfer-instruction-v1.yaml' - ), - - // V2 Specs - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-allocation-v2/openapi/allocation-v2.yaml' - ), - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-allocation-instruction-v2/openapi/allocation-instruction-v2.yaml' - ), - path.join( - __dirname, - '../../../../../token-standard/splice-api-token-transfer-instruction-v2/openapi/transfer-instruction-v2.yaml' - ), -]; - -const MinimalOpenApiSchema = z.object({ paths: z.object({}).catchall(z.unknown()).default({}) }); - -/** - * Read scan.yaml OpenAPI paths into normalized `/api/scan...` endpoint prefixes. - * Keep only the static prefix before the first `{...}` segment if the path contains parameters. - * This preserves only the segment for which simple string prefix matching works. - */ -export function parseScanYamlEndpoints(): string[] { - const yaml = MinimalOpenApiSchema.parse(readAndParseYaml(scanYamlPath)); - const paths = yaml.paths; - - const endpoints = new Set(); - - for (const path of Object.keys(paths)) { - // Prepend /api/scan prefix - let fullPath = '/api/scan' + path; - - // Strip to segment before first { - const paramIndex = fullPath.indexOf('{'); - if (paramIndex !== -1) { - // Find the / before the { - const lastSlash = fullPath.lastIndexOf('/', paramIndex); - fullPath = fullPath.substring(0, lastSlash); - } - - endpoints.add(fullPath); - } - - return Array.from(endpoints).sort(); -} - -/** - * Read all token registry standard OpenAPI paths into normalized `/registry/...` endpoint prefixes. - */ -export function parseTokenRegistrySpecEndpoints(): string[] { - const endpoints = new Set(); - - for (const yamlPath of tokenRegistryYamlPaths) { - const yaml = MinimalOpenApiSchema.parse(readAndParseYaml(yamlPath)); - const paths = yaml.paths; - - for (let fullPath of Object.keys(paths)) { - const paramIndex = fullPath.indexOf('{'); - if (paramIndex !== -1) { - const lastSlash = fullPath.lastIndexOf('/', paramIndex); - fullPath = fullPath.substring(0, lastSlash); - } - - endpoints.add(fullPath); - } - } - - return Array.from(endpoints).sort(); -} diff --git a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts index c4d69f9f566..55041413d4d 100644 --- a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts +++ b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.test.ts @@ -1,24 +1,34 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { expect, jest, test } from '@jest/globals'; +import { expect, test } from '@jest/globals'; import { + buildEndpointRateLimitDescriptors, + buildGlobalPerIpRateLimitAction, + buildGlobalPerIpRateLimitDescriptors, + buildHttpFilterPatches, + buildPerEndpointPerIpRateLimitDescriptors, buildRateLimitActions, - buildRateLimitDescriptors, + buildRateLimitFilters, + buildTypedPerFilterConfig, + extractPathPrefixes, + globalPerIpRateLimitFilterName, + globalPerIpRateLimitStatPrefix, + globalRateLimitFilterName, + globalRateLimitStatPrefix, parseFillIntervalMs, - validateIpLimits, + perEndpointPerIpRateLimitFilterName, + perEndpointPerIpRateLimitStatPrefix, + perEndpointRateLimitFilterName, + perEndpointRateLimitStatPrefix, + rateLimiterLabel, + rateLimiterMetricPrefix, + rateLimiterMetricRelabelings, + validateEffectiveRateLimits, + validateGrpcPathPrefixes, validateTokenBuckets, } from './envoyRateLimiter'; -jest.mock('@canton-network/splice-pulumi-common/src/config/envConfig', () => ({ - __esModule: true, - spliceEnvConfig: { - requireEnv() { - return 'dummy'; - }, - }, -})); - const baseLimits = { maxTokens: 720, tokensPerFill: 720, @@ -31,139 +41,374 @@ const perIpLimits = { fillInterval: '60s', }; -test('buildRateLimitDescriptors generates per-endpoint and generic per-IP descriptors', () => { - const descriptors = buildRateLimitDescriptors({ - '/registry/metadata/v1/info': { - name: 'registry-metadata-info', - type: 'limited', - ...baseLimits, - perIpLimits, - }, - }); +const globalLimits = { + maxTokens: 10000, + tokensPerFill: 10000, + fillInterval: '60s', +}; - expect(descriptors).toHaveLength(2); - expect(descriptors[0]).toEqual({ - entries: [{ key: 'header_match', value: 'registry-metadata-info' }], - token_bucket: { - max_tokens: 720, - tokens_per_fill: 720, - fill_interval: '60s', - }, +const globalPerIpLimits = { + maxTokens: 1000, + tokensPerFill: 1000, + fillInterval: '60s', +}; + +const unlimited = { + max_tokens: 4294967295, + tokens_per_fill: 4294967295, + fill_interval: '60s', +}; + +// a single endpoint with per-IP limits, the simplest configuration exercising all four limits +const singleEndpointRateLimits = { + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited' as const, + ...baseLimits, + perIpLimits, + }, +}; + +const multiIpOverride = { + 'multi-validators': { + ips: ['192.68.78.51', '192.68.78.52'], + maxTokens: 250, + tokensPerFill: 250, + fillInterval: '60s', + }, +}; + +test('extractPathPrefixes keeps only the externally reachable prefixes', () => { + expect( + extractPathPrefixes({ + '/api/scan/v0/acs': { name: 'acs', type: 'limited', ...baseLimits }, + '/registry/metadata/v1/info': { name: 'info', type: 'limited', ...baseLimits }, + '/api/internal/status': { name: 'status', type: 'limited', ...baseLimits }, + }) + ).toEqual(['/api/scan/v0/acs', '/registry/metadata/v1/info']); + expect(extractPathPrefixes(undefined)).toEqual([]); +}); + +test('validateGrpcPathPrefixes accepts gRPC method paths', () => { + expect(() => + validateGrpcPathPrefixes({ + '/com.digitalasset.canton.sequencer.api.v30.SequencerService/': { + name: 'sequencer-service', + type: 'limited', + ...baseLimits, + }, + '/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe': { + name: 'sequencer-subscribe', + type: 'limited', + ...baseLimits, + }, + }) + ).not.toThrow(); + expect(() => validateGrpcPathPrefixes(undefined)).not.toThrow(); +}); + +test('validateGrpcPathPrefixes rejects prefixes that could never match a gRPC request', () => { + // HTTP endpoints, a missing leading slash, a service without a package and a bare service name + // all fail to match the `//` path gRPC actually sends, so the + // limit would silently never be enforced. + [ + '/api/scan/v0/acs', + 'com.digitalasset.canton.sequencer.api.v30.SequencerService/', + '/SequencerService/Subscribe', + '/com.digitalasset.canton.sequencer.api.v30.SequencerService', + ].forEach(pathPrefix => { + expect(() => + validateGrpcPathPrefixes({ + [pathPrefix]: { name: 'some-limit', type: 'limited', ...baseLimits }, + }) + ).toThrow(/invalid gRPC path prefixes/); }); - expect(descriptors[1]).toEqual({ - entries: [ - { key: 'header_match', value: 'registry-metadata-info' }, - { key: 'masked_remote_address' }, - ], - token_bucket: { - max_tokens: 120, - tokens_per_fill: 120, - fill_interval: '60s', +}); + +test('buildEndpointRateLimitDescriptors generates one bucket per endpoint', () => { + expect(buildEndpointRateLimitDescriptors(singleEndpointRateLimits)).toEqual([ + { + entries: [{ key: 'header_match', value: 'registry-metadata-info' }], + token_bucket: { + max_tokens: 720, + tokens_per_fill: 720, + fill_interval: '60s', + }, }, - }); + ]); }); -test('buildRateLimitDescriptors emits named IP overrides before generic per-IP descriptor', () => { - const descriptors = buildRateLimitDescriptors({ - '/registry/metadata/v1/info': { - name: 'registry-metadata-info', - type: 'limited', - ...baseLimits, - perIpLimits: { - ...perIpLimits, - overrides: { - 'single-validator': { - ips: ['192.68.78.50'], - maxTokens: 220, - tokensPerFill: 220, - fillInterval: '60s', - }, - }, +test('buildPerEndpointPerIpRateLimitDescriptors generates a bucket per endpoint and client IP', () => { + expect(buildPerEndpointPerIpRateLimitDescriptors(singleEndpointRateLimits)).toEqual([ + { + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address' }, + ], + token_bucket: { + max_tokens: 120, + tokens_per_fill: 120, + fill_interval: '60s', }, }, - }); + ]); +}); - expect(descriptors).toHaveLength(3); - expect(descriptors[0]).toEqual( - expect.objectContaining({ - entries: [{ key: 'header_match', value: 'registry-metadata-info' }], +test('buildPerEndpointPerIpRateLimitDescriptors emits one descriptor per overridden IP, before the wildcard one', () => { + expect( + buildPerEndpointPerIpRateLimitDescriptors({ + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + perIpLimits: { ...perIpLimits, overrides: multiIpOverride }, + }, }) - ); - expect(descriptors[1]).toEqual({ - entries: [ - { key: 'header_match', value: 'registry-metadata-info' }, - { key: 'masked_remote_address', value: '192.68.78.50/32' }, - ], - token_bucket: { - max_tokens: 220, - tokens_per_fill: 220, - fill_interval: '60s', + ).toEqual([ + { + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address', value: '192.68.78.51/32' }, + ], + token_bucket: { max_tokens: 250, tokens_per_fill: 250, fill_interval: '60s' }, }, - }); - expect(descriptors[2]).toEqual( - expect.objectContaining({ + { + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address', value: '192.68.78.52/32' }, + ], + token_bucket: { max_tokens: 250, tokens_per_fill: 250, fill_interval: '60s' }, + }, + { entries: [ { key: 'header_match', value: 'registry-metadata-info' }, { key: 'masked_remote_address' }, ], - }) - ); + token_bucket: { max_tokens: 120, tokens_per_fill: 120, fill_interval: '60s' }, + }, + ]); }); -test('buildRateLimitDescriptors emits descriptors for named overrides with multiple ips', () => { - const descriptors = buildRateLimitDescriptors({ - '/registry/metadata/v1/info': { - name: 'registry-metadata-info', - type: 'limited', - ...baseLimits, - perIpLimits: { - ...perIpLimits, - overrides: { - 'multi-validators': { - ips: ['192.68.78.51', '192.68.78.52'], - maxTokens: 250, - tokensPerFill: 250, - fillInterval: '60s', - }, - }, - }, +test('buildGlobalPerIpRateLimitDescriptors emits a wildcard bucket, preceded by the IP overrides', () => { + // without overrides there is a single bucket per observed client IP + expect(buildGlobalPerIpRateLimitDescriptors(globalPerIpLimits)).toEqual([ + { + entries: [{ key: 'masked_remote_address' }], + token_bucket: { max_tokens: 1000, tokens_per_fill: 1000, fill_interval: '60s' }, }, - }); + ]); - expect(descriptors).toHaveLength(4); - expect(descriptors[1]).toEqual({ - entries: [ - { key: 'header_match', value: 'registry-metadata-info' }, - { key: 'masked_remote_address', value: '192.68.78.51/32' }, - ], - token_bucket: { - max_tokens: 250, - tokens_per_fill: 250, - fill_interval: '60s', + expect( + buildGlobalPerIpRateLimitDescriptors({ ...globalPerIpLimits, overrides: multiIpOverride }) + ).toEqual([ + { + entries: [{ key: 'masked_remote_address', value: '192.68.78.51/32' }], + token_bucket: { max_tokens: 250, tokens_per_fill: 250, fill_interval: '60s' }, + }, + { + entries: [{ key: 'masked_remote_address', value: '192.68.78.52/32' }], + token_bucket: { max_tokens: 250, tokens_per_fill: 250, fill_interval: '60s' }, }, + { + entries: [{ key: 'masked_remote_address' }], + token_bucket: { max_tokens: 1000, tokens_per_fill: 1000, fill_interval: '60s' }, + }, + ]); +}); + +test('buildRateLimitFilters installs one filter per limit, from the most to the least specific', () => { + const config = buildTypedPerFilterConfig( + buildRateLimitFilters(globalLimits, globalPerIpLimits, singleEndpointRateLimits) + ); + + // envoy consumes at most one descriptor bucket per filter, so limits that must all be + // respected are enforced by separate filters. They run in this order, so that a request + // rejected by a specific limit does not consume the tokens of the broader buckets. + expect(Object.keys(config)).toEqual([ + perEndpointPerIpRateLimitFilterName, + perEndpointRateLimitFilterName, + globalPerIpRateLimitFilterName, + globalRateLimitFilterName, + ]); + + const filterConfig = (name: string) => config[name] as Record; + + // only the global filter limits through its default bucket, which every request consumes + expect(filterConfig(globalRateLimitFilterName).always_consume_default_token_bucket).toBe(true); + expect(filterConfig(globalRateLimitFilterName).token_bucket).toEqual({ + max_tokens: 10000, + tokens_per_fill: 10000, + fill_interval: '60s', }); - expect(descriptors[2]).toEqual({ - entries: [ - { key: 'header_match', value: 'registry-metadata-info' }, - { key: 'masked_remote_address', value: '192.68.78.52/32' }, - ], - token_bucket: { - max_tokens: 250, - tokens_per_fill: 250, - fill_interval: '60s', + // and it enforces nothing else, so that a rejection is unambiguously attributed to it + expect(filterConfig(globalRateLimitFilterName).descriptors).toEqual([]); + + // the filters whose limits are all expressed as descriptors must not limit the requests + // matching none of them + [ + perEndpointPerIpRateLimitFilterName, + perEndpointRateLimitFilterName, + globalPerIpRateLimitFilterName, + ].forEach(name => { + expect(filterConfig(name).always_consume_default_token_bucket).toBe(false); + expect(filterConfig(name).token_bucket).toEqual(unlimited); + }); + + expect(filterConfig(perEndpointRateLimitFilterName).descriptors).toEqual([ + { + entries: [{ key: 'header_match', value: 'registry-metadata-info' }], + token_bucket: { max_tokens: 720, tokens_per_fill: 720, fill_interval: '60s' }, + }, + ]); + expect(filterConfig(perEndpointPerIpRateLimitFilterName).descriptors).toEqual([ + { + entries: [ + { key: 'header_match', value: 'registry-metadata-info' }, + { key: 'masked_remote_address' }, + ], + token_bucket: { max_tokens: 120, tokens_per_fill: 120, fill_interval: '60s' }, + }, + ]); + expect(filterConfig(globalPerIpRateLimitFilterName).descriptors).toEqual([ + { + entries: [{ key: 'masked_remote_address' }], + token_bucket: { max_tokens: 1000, tokens_per_fill: 1000, fill_interval: '60s' }, }, + ]); +}); + +test('the per-endpoint filters are omitted when nothing is configured for them', () => { + // no endpoint configures per-IP limits + expect( + buildRateLimitFilters(globalLimits, globalPerIpLimits, { + '/registry/metadata/v1/info': { + name: 'registry-metadata-info', + type: 'limited', + ...baseLimits, + }, + }).map(filter => filter.name) + ).toEqual([ + perEndpointRateLimitFilterName, + globalPerIpRateLimitFilterName, + globalRateLimitFilterName, + ]); + + // no endpoint limits at all + expect( + buildRateLimitFilters(globalLimits, globalPerIpLimits, {}).map(filter => filter.name) + ).toEqual([globalPerIpRateLimitFilterName, globalRateLimitFilterName]); +}); + +test('buildHttpFilterPatches keeps the filter order by pinning the insertion point to the router', () => { + const filters = buildRateLimitFilters(globalLimits, globalPerIpLimits, singleEndpointRateLimits); + const patches = buildHttpFilterPatches(filters, 5008) as { + applyTo: string; + match: { + listener: { portNumber: number; filterChain: { filter: { subFilter?: { name: string } } } }; + }; + patch: { operation: string; value: { name: string; typed_config: { value: unknown } } }; + }[]; + + // istio applies the patches in order and each of them inserts right before the router, which is + // always the last filter of the chain, so the chain order is the order of buildRateLimitFilters. + // Without the subFilter match istio would insert every filter before the *first* one, which + // would silently reverse them and make the global limit run first. + patches.forEach(patch => { + expect(patch.applyTo).toEqual('HTTP_FILTER'); + expect(patch.patch.operation).toEqual('INSERT_BEFORE'); + expect(patch.match.listener.filterChain.filter.subFilter).toEqual({ + name: 'envoy.filters.http.router', + }); + // only the port carrying the externally reachable API is rate limited; the workload's other + // inbound listeners (e.g. the sequencer's admin and peer-to-peer ports, which istio also + // treats as HTTP) must keep their filter chain untouched + expect(patch.match.listener.portNumber).toEqual(5008); + }); + expect(patches.map(patch => patch.patch.value.name)).toEqual(filters.map(filter => filter.name)); + // the filters are configured per route, the chain only declares them with their stat prefix + expect(patches[0].patch.value.typed_config.value).toEqual({ + stat_prefix: perEndpointPerIpRateLimitStatPrefix, }); }); +test('the filters are distinguishable in the metrics and in the access logs', () => { + const filters = buildRateLimitFilters(globalLimits, globalPerIpLimits, singleEndpointRateLimits); + + // envoy does not label the local rate limit metrics, the stat prefix is the only distinction, + // so no two limits may share one + expect(filters.map(f => f.statPrefix)).toEqual([ + perEndpointPerIpRateLimitStatPrefix, + perEndpointRateLimitStatPrefix, + globalPerIpRateLimitStatPrefix, + globalRateLimitStatPrefix, + ]); + expect(new Set(filters.map(f => f.statPrefix)).size).toEqual(filters.length); + + // and the response header identifies the limit that rejected a request in the access logs + const config = buildTypedPerFilterConfig(filters); + const headerValues = filters.map(filter => { + const filterConfig = config[filter.name] as Record; + const headers = filterConfig.response_headers_to_add as { + header: { key: string; value: string }; + }[]; + expect(headers[0].header.key).toEqual('x-local-rate-limit'); + return headers[0].header.value; + }); + // the same names are used for the `limiter` metric label, so that a rejection can be correlated + // between the metrics and the access logs + expect(headerValues).toEqual( + filters.map(filter => filter.statPrefix.replace(/^.*_limiter_/, '')) + ); +}); + +test('the metric relabelings merge the filter metrics into one metric labeled by limiter', () => { + const [extractLimiter, normalizeName] = rateLimiterMetricRelabelings; + + const relabel = (metric: string) => { + const limiter = new RegExp(`^${extractLimiter.regex}$`).exec(metric); + const name = new RegExp(`^${normalizeName.regex}$`).exec(metric); + return { + [rateLimiterLabel]: limiter?.[1], + __name__: name ? `${rateLimiterMetricPrefix}_${name[1]}` : undefined, + }; + }; + + const filters = buildRateLimitFilters(globalLimits, globalPerIpLimits, singleEndpointRateLimits); + + // every limit gets its own `limiter` label, in particular the global and the per-endpoint ones, + // which are enforced by two separate filters + expect( + filters.map(filter => relabel(`envoy_${filter.statPrefix}_http_local_rate_limit_enforced`)) + ).toEqual([ + { limiter: 'endpoint_per_ip', __name__: 'envoy_http_local_rate_limit_enforced' }, + { limiter: 'endpoint', __name__: 'envoy_http_local_rate_limit_enforced' }, + { limiter: 'per_ip', __name__: 'envoy_http_local_rate_limit_enforced' }, + { limiter: 'global', __name__: 'envoy_http_local_rate_limit_enforced' }, + ]); + // all the counters of the filter are covered + expect( + ['enabled', 'ok', 'rate_limited', 'enforced'].map(counter => + relabel(`envoy_${globalPerIpRateLimitStatPrefix}_http_local_rate_limit_${counter}`) + ) + ).toEqual( + ['enabled', 'ok', 'rate_limited', 'enforced'].map(counter => ({ + limiter: 'per_ip', + __name__: `envoy_http_local_rate_limit_${counter}`, + })) + ); + // and unrelated metrics are left alone + expect(relabel('istio_requests_total')).toEqual({ limiter: undefined, __name__: undefined }); +}); + test('buildRateLimitActions emits per-endpoint and per-IP actions', () => { - const actions = buildRateLimitActions({ - '/registry/metadata/v1/info': { - name: 'registry-metadata-info', - type: 'limited', - ...baseLimits, - perIpLimits, + const actions = buildRateLimitActions(singleEndpointRateLimits); + const pathMatch = { + name: ':path', + string_match: { + prefix: '/registry/metadata/v1/info', + ignore_case: true, }, - }); + }; expect(actions).toHaveLength(2); expect(actions[0]).toEqual({ @@ -172,15 +417,7 @@ test('buildRateLimitActions emits per-endpoint and per-IP actions', () => { header_value_match: { descriptor_value: 'registry-metadata-info', expect_match: true, - headers: [ - { - name: ':path', - string_match: { - prefix: '/registry/metadata/v1/info', - ignore_case: true, - }, - }, - ], + headers: [pathMatch], }, }, ], @@ -191,15 +428,7 @@ test('buildRateLimitActions emits per-endpoint and per-IP actions', () => { header_value_match: { descriptor_value: 'registry-metadata-info', expect_match: true, - headers: [ - { - name: ':path', - string_match: { - prefix: '/registry/metadata/v1/info', - ignore_case: true, - }, - }, - ], + headers: [pathMatch], }, }, { @@ -213,58 +442,175 @@ test('buildRateLimitActions emits per-endpoint and per-IP actions', () => { }); }); -test('validateIpLimits throws on duplicate IP between two named overrides', () => { - expect(() => - validateIpLimits('/registry/metadata/v1/info', { - name: 'registry-metadata-info', +test('buildGlobalPerIpRateLimitAction keys only on the non-spoofable client address', () => { + expect(buildGlobalPerIpRateLimitAction()).toEqual({ + actions: [ + { + masked_remote_address: { + v4_prefix_mask_len: 32, + v6_prefix_mask_len: 128, + }, + }, + ], + }); +}); + +test('buildRateLimitActions makes nested path prefixes mutually exclusive', () => { + const actions = buildRateLimitActions({ + '/registry/transfer-instruction/v1': { + name: 'registry-transfer-instruction', type: 'limited', ...baseLimits, - perIpLimits: { - ...perIpLimits, - overrides: { - 'group-a': { - ips: ['192.68.78.50', '192.68.78.51'], - maxTokens: 250, - tokensPerFill: 250, - fillInterval: '60s', - }, - 'group-b': { - ips: ['192.68.78.51'], - maxTokens: 250, - tokensPerFill: 250, - fillInterval: '60s', - }, + }, + '/registry/transfer-instruction/v1/transfer-factory': { + name: 'registry-transfer-factory', + type: 'limited', + ...baseLimits, + }, + }) as { actions: { header_value_match: { descriptor_value: string; headers: unknown[] } }[] }[]; + + // the less specific endpoint excludes the requests matched by the nested one, so that a + // request never generates two per-endpoint descriptors (envoy would only consume one of them) + expect(actions[0].actions[0].header_value_match).toEqual({ + descriptor_value: 'registry-transfer-instruction', + expect_match: true, + headers: [ + { + name: ':path', + string_match: { prefix: '/registry/transfer-instruction/v1', ignore_case: true }, + }, + { + name: ':path', + string_match: { + prefix: '/registry/transfer-instruction/v1/transfer-factory', + ignore_case: true, }, + invert_match: true, }, - }) - ).toThrow("192.68.78.51 (in override 'group-b')"); + ], + }); + // and the most specific endpoint excludes nothing + expect(actions[1].actions[0].header_value_match.headers).toEqual([ + { + name: ':path', + string_match: { + prefix: '/registry/transfer-instruction/v1/transfer-factory', + ignore_case: true, + }, + }, + ]); }); -test('validateIpLimits accepts unique IPs across named overrides', () => { - expect(() => - validateIpLimits('/registry/metadata/v1/info', { - name: 'registry-metadata-info', - type: 'limited', +const envoyFilterArgs = { + namespace: 'sv-1', + appLabel: 'scan-app', + inboundPort: 5012, + globalLimits, + globalPerIpLimits, + rateLimits: { + '/api/scan/v0/acs': { + name: 'acs', + type: 'limited' as const, ...baseLimits, - perIpLimits: { - ...perIpLimits, + perIpLimits, + }, + }, +}; + +test('validateEffectiveRateLimits validates the global per-IP limits and their overrides', () => { + expect(() => + validateEffectiveRateLimits({ + ...envoyFilterArgs, + globalPerIpLimits: { maxTokens: 1000, tokensPerFill: 1000, fillInterval: '90s' }, + }) + ).toThrow('globalPerIpLimits: fillInterval'); + + expect(() => + validateEffectiveRateLimits({ + ...envoyFilterArgs, + globalPerIpLimits: { + ...globalPerIpLimits, overrides: { 'single-validator': { ips: ['192.68.78.50'], - maxTokens: 220, - tokensPerFill: 220, - fillInterval: '60s', - }, - 'multi-validators': { - ips: ['192.68.78.51', '192.68.78.52'], - maxTokens: 250, - tokensPerFill: 250, - fillInterval: '60s', + maxTokens: 5000, + tokensPerFill: 5000, + fillInterval: '90s', }, }, }, }) - ).not.toThrow(); + ).toThrow("globalPerIpLimits override 'single-validator'"); +}); + +test('validateEffectiveRateLimits rejects an IP listed in two overrides', () => { + const duplicated = { + 'group-a': { + ips: ['192.68.78.50', '192.68.78.51'], + maxTokens: 5000, + tokensPerFill: 5000, + fillInterval: '60s', + }, + 'group-b': { + ips: ['192.68.78.51'], + maxTokens: 5000, + tokensPerFill: 5000, + fillInterval: '60s', + }, + }; + + // only one descriptor is consumed per client IP, so an IP listed twice would silently get the + // limits of one of the two overrides + expect(() => + validateEffectiveRateLimits({ + ...envoyFilterArgs, + globalPerIpLimits: { ...globalPerIpLimits, overrides: duplicated }, + }) + ).toThrow( + "globalPerIpLimits: duplicate IPs in per-IP rate limits: 192.68.78.51 (in override 'group-b')" + ); + + // the per-endpoint per-IP overrides are validated the same way, and reported by path + expect(() => + validateEffectiveRateLimits({ + ...envoyFilterArgs, + rateLimits: { + '/api/scan/v0/acs': { + name: 'acs', + type: 'limited' as const, + ...baseLimits, + perIpLimits: { ...perIpLimits, overrides: duplicated }, + }, + }, + }) + ).toThrow('/api/scan/v0/acs: duplicate IPs in per-IP rate limits: 192.68.78.51'); +}); + +test('validateEffectiveRateLimits rejects reserved descriptor names', () => { + expect(() => + validateEffectiveRateLimits({ + ...envoyFilterArgs, + rateLimits: { + '/api/scan/v0/acs': { + name: 'masked_remote_address', + type: 'limited' as const, + ...baseLimits, + }, + }, + }) + ).toThrow('use reserved name'); +}); + +test('validateEffectiveRateLimits rejects two endpoints sharing a name', () => { + expect(() => + validateEffectiveRateLimits({ + ...envoyFilterArgs, + rateLimits: { + '/api/scan/v0/acs': { name: 'scan', type: 'limited' as const, ...baseLimits }, + '/api/scan/v0/holdings': { name: 'scan', type: 'limited' as const, ...baseLimits }, + }, + }) + ).toThrow('duplicate rate limit names: scan'); }); test('parseFillIntervalMs parses protobuf durations and rejects other formats', () => { @@ -340,3 +686,25 @@ test('validateTokenBuckets rejects intervals that envoy would NACK', () => { }) ).toThrow("perIpLimits override 'single-validator'"); }); + +test('gRPC rejections are reported as RESOURCE_EXHAUSTED, HTTP ones as 429', () => { + const filters = buildRateLimitFilters(globalLimits, globalPerIpLimits, singleEndpointRateLimits); + + // envoy answers a rate limited gRPC call with HTTP 200 and a gRPC status, so the rejection is + // only recognizable (by the client and in the access logs) via that status + const grpcConfig = buildTypedPerFilterConfig(filters, 'grpc'); + filters.forEach(filter => { + expect( + (grpcConfig[filter.name] as Record).rate_limited_as_resource_exhausted + ).toEqual(true); + }); + + // HTTP requests are rejected with 429, the default UNAVAILABLE/RESOURCE_EXHAUSTED distinction + // does not apply + const httpConfig = buildTypedPerFilterConfig(filters); + filters.forEach(filter => { + expect( + (httpConfig[filter.name] as Record).rate_limited_as_resource_exhausted + ).toEqual(false); + }); +}); diff --git a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts index 8844d0305bb..d4cff5b8410 100644 --- a/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts +++ b/cluster/pulumi/common/src/ratelimit/envoyRateLimiter.ts @@ -3,7 +3,6 @@ import * as k8s from '@pulumi/kubernetes'; import * as pulumi from '@pulumi/pulumi'; -import { parseScanYamlEndpoints, parseTokenRegistrySpecEndpoints } from '../config/scanEndpoints'; import { localRateLimitedHeader } from './rateLimitHeaders'; interface Limits { @@ -12,7 +11,7 @@ interface Limits { fillInterval: string; } -interface PerIpLimits extends Limits { +export interface PerIpLimits extends Limits { overrides?: Record; } @@ -21,21 +20,6 @@ interface MatchedLimits extends Limits { perIpLimits?: PerIpLimits; } -interface Banned { - type: 'banned'; -} - -interface Unlimited { - type: 'unlimited'; -} - -type RateLimitConfig = MatchedLimits | Banned | Unlimited; - -export interface PathPrefixInfo { - pathPrefix: string; - isBanned: boolean; -} - interface LocalLimits { [pathPrefix: string]: LocalLimit; } @@ -62,67 +46,154 @@ const minFillIntervalMs = 50; // uint32 in envoy's TokenBucket proto const maxTokenValue = 4294967295; -interface RateLimitEnvoyFilterArgs extends PerEndpointLimits { +// Within one local rate limit filter, envoy consumes a token from at most *one* descriptor +// bucket per request (the first descriptor matching the descriptors generated by the route's +// rate limit actions), plus a token from the filter's own default token bucket. +// Limits that must all be respected therefore live in separate filters, each of which consumes +// a token of its own bucket for every request. There is exactly one filter per limit, so that +// the metrics and the access logs can attribute a rejection to the limit that caused it: +// - `perEndpointPerIpRateLimitFilterName`: the per-endpoint, per-client-IP limits (including +// the per-IP overrides). +// - `perEndpointRateLimitFilterName`: the per-endpoint limits, one bucket per configured +// endpoint. +// - `globalPerIpRateLimitFilterName`: a single wildcard descriptor keyed on the client IP, i.e. +// one bucket per client IP, consumed by every request. +// - `globalRateLimitFilterName`: the global limit, consumed by every request. +// The descriptor sets of the filters are disjoint, so they can share the route level rate limit +// actions without interfering with each other. +// The filters run in that order, from the most to the least specific limit, so that a request +// rejected by a specific limit does not consume the tokens of the broader buckets: a client +// hammering a single endpoint burns its own per-endpoint per-IP bucket and leaves the global +// bucket to everybody else. +export const globalRateLimitFilterName = 'envoy.filters.http.local_ratelimit'; +export const perEndpointRateLimitFilterName = 'envoy.filters.http.local_ratelimit.endpoint'; +export const globalPerIpRateLimitFilterName = 'envoy.filters.http.local_ratelimit.per_ip'; +export const perEndpointPerIpRateLimitFilterName = + 'envoy.filters.http.local_ratelimit.endpoint_per_ip'; + +// Envoy emits `.http_local_rate_limit.{enabled,ok,rate_limited,enforced}` per filter +// and does not label them, so the stat prefix is the only thing telling the filters apart in the +// metrics. Every filter therefore uses `_` as its stat prefix, and +// prometheus rewrites the scraped series into a single metric name carrying the limiter in a +// label (see `rateLimiterMetricRelabelings`): +// envoy_http_local_rate_limiter_per_ip_http_local_rate_limit_enforced +// -> envoy_http_local_rate_limit_enforced{limiter="per_ip"} +export const rateLimiterStatPrefix = 'http_local_rate_limiter'; +// the metric name all the limiters share after relabeling +export const rateLimiterMetricPrefix = 'envoy_http_local_rate_limit'; +// the label the limiter ends up in after relabeling +export const rateLimiterLabel = 'limiter'; + +// The name of each limit, used both as the value of the `x-local-rate-limit` response header (which +// identifies the limit that rejected a request in the sidecar access logs, as the metrics cannot +// attribute a single request to a limit) and as the value of the `limiter` metric label. +export const globalRateLimitName = 'global'; +export const perEndpointRateLimitName = 'endpoint'; +export const globalPerIpRateLimitName = 'per_ip'; +export const perEndpointPerIpRateLimitName = 'endpoint_per_ip'; + +export const globalRateLimitStatPrefix = `${rateLimiterStatPrefix}_${globalRateLimitName}`; +export const perEndpointRateLimitStatPrefix = `${rateLimiterStatPrefix}_${perEndpointRateLimitName}`; +export const globalPerIpRateLimitStatPrefix = `${rateLimiterStatPrefix}_${globalPerIpRateLimitName}`; +export const perEndpointPerIpRateLimitStatPrefix = `${rateLimiterStatPrefix}_${perEndpointPerIpRateLimitName}`; + +/** + * The prometheus `metric_relabel_configs` turning the per-filter metric names into a single metric + * name labeled by limiter. + */ +export const rateLimiterMetricRelabelings = [ + { + // extract the limiter from the metric name, must run before the name is rewritten + sourceLabels: ['__name__'], + regex: `envoy_${rateLimiterStatPrefix}_(.+)_http_local_rate_limit_.+`, + targetLabel: rateLimiterLabel, + replacement: '$1', + action: 'replace', + }, + { + // and normalize the metric name, so that all the limiters share it + sourceLabels: ['__name__'], + regex: `envoy_${rateLimiterStatPrefix}_.+_http_local_rate_limit_(.+)`, + targetLabel: '__name__', + replacement: `${rateLimiterMetricPrefix}_$1`, + action: 'replace', + }, +]; + +export interface RateLimitEnvoyFilterArgs extends PerEndpointLimits { namespace: string; appLabel: pulumi.Input; inboundPort: pulumi.Input; - /** - * Used when no descriptors match the request. - * */ + // consumed by every request globalLimits: Limits; + + // consumed by every request, keyed on the client IP, with optional per-IP overrides + globalPerIpLimits: PerIpLimits; + + // the protocol spoken on the rate limited port, `http` by default + protocol?: RateLimitProtocol; } +// The protocol the rate limited workload speaks. It determines how the configured path prefixes are +// validated, and how envoy reports a rejection: an HTTP request rejected by the local rate limit +// filter gets `429`, whereas a gRPC call gets HTTP `200` and a gRPC status in the response, so the +// two are recognized differently in the metrics and the access logs. +export type RateLimitProtocol = 'http' | 'grpc'; + +// The gRPC status code (`RESOURCE_EXHAUSTED`) envoy returns for a rate limited gRPC call, see +// `rate_limited_as_resource_exhausted` in `localRateLimitFilterConfig`. +export const rateLimitedGrpcStatus = 8; + export interface PerEndpointLimits { // all the rate limits must be respected, there's an AND relationship between them - rateLimits?: LocalLimits; + rateLimits?: LocalLimits; } -export function extractPathPrefixes( - rateLimits?: PerEndpointLimits['rateLimits'] -): PathPrefixInfo[] { +export function extractPathPrefixes(rateLimits?: PerEndpointLimits['rateLimits']): string[] { if (!rateLimits) { return []; } - return Object.entries(rateLimits) - .map(([pathPrefix, rl]) => { - const isBanned = rl.type === 'banned'; - return { pathPrefix, isBanned }; - }) - .filter( - info => info.pathPrefix.startsWith('/api/scan') || info.pathPrefix.startsWith('/registry') - ); -} - -function validateEndpointCoverage( - scanEndpoints: string[], - configuredScanPrefixes: string[] -): { missing: string[]; orphaned: string[] } { - // Check for missing prefixes - const missing = scanEndpoints.filter( - endpoint => !configuredScanPrefixes.some(prefix => endpoint.startsWith(prefix)) + return Object.keys(rateLimits).filter( + pathPrefix => pathPrefix.startsWith('/api/scan') || pathPrefix.startsWith('/registry') ); +} - // Check for orphaned prefixes - const orphaned = configuredScanPrefixes.filter( - prefix => !scanEndpoints.some(endpoint => endpoint.startsWith(prefix)) +/** + * Checks that the configured path prefixes can actually match a gRPC request. + * + * gRPC maps every call onto the HTTP/2 path `//`, which is what + * the route level `header_match` actions match on. A prefix that is not of that shape (e.g. an + * HTTP endpoint such as `/api/scan/v0/...`, or a bare service name without the leading slash) + * would never match any request, so the limit would silently never be enforced. + */ +export function validateGrpcPathPrefixes(rateLimits?: PerEndpointLimits['rateLimits']): void { + // `//` optionally followed by a method name; the service must be a dotted, non-empty + // fully qualified protobuf name. + const grpcPathPrefix = /^\/[A-Za-z_][\w]*(\.[A-Za-z_][\w]*)+\/[A-Za-z_]?\w*$/; + const invalid = Object.keys(rateLimits || {}).filter( + pathPrefix => !grpcPathPrefix.test(pathPrefix) ); - - return { missing, orphaned }; + if (invalid.length > 0) { + throw new Error( + `invalid gRPC path prefixes: ${invalid.join(', ')}; expected '//' ` + + `(a trailing '/' matches every method of the service), otherwise the limit would never match a request` + ); + } } -export function validateIpLimits(pathPrefix: string, rateLimit: LocalLimit): void { - if (!rateLimit.perIpLimits) { +export function validateIpLimits(context: string, perIpLimits?: PerIpLimits): void { + if (!perIpLimits) { return; } const seenIps = new Set(); const duplicates: string[] = []; - Object.entries(rateLimit.perIpLimits.overrides || {}).forEach(([overrideKey, override]) => { + Object.entries(perIpLimits.overrides || {}).forEach(([overrideKey, override]) => { override.ips.forEach(ip => { if (seenIps.has(ip)) { duplicates.push(`${ip} (in override '${overrideKey}')`); @@ -133,7 +204,7 @@ export function validateIpLimits(pathPrefix: string, rateLimit: LocalLimit 0) { - throw new Error(`${pathPrefix}: duplicate IPs in per-IP rate limits: ${duplicates.join(', ')}`); + throw new Error(`${context}: duplicate IPs in per-IP rate limits: ${duplicates.join(', ')}`); } } @@ -177,30 +248,42 @@ function validateLimits(context: string, limits: Limits, globalFillIntervalMs?: }); } +function validatePerIpTokenBuckets( + context: string, + perIpLimits: PerIpLimits, + globalFillIntervalMs?: number +): void { + validateLimits(context, perIpLimits, globalFillIntervalMs); + Object.entries(perIpLimits.overrides || {}).forEach(([name, override]) => + validateLimits(`${context} override '${name}'`, override, globalFillIntervalMs) + ); +} + export function validateTokenBuckets( globalLimits: Limits, - effectiveRateLimits: LocalLimits + effectiveRateLimits: LocalLimits, + globalPerIpLimits?: PerIpLimits ): void { validateLimits('globalLimits', globalLimits); const globalFillIntervalMs = parseFillIntervalMs(globalLimits.fillInterval, 'globalLimits'); + if (globalPerIpLimits) { + validatePerIpTokenBuckets('globalPerIpLimits', globalPerIpLimits, globalFillIntervalMs); + } Object.entries(effectiveRateLimits).forEach(([pathPrefix, rateLimit]) => { validateLimits(pathPrefix, rateLimit, globalFillIntervalMs); if (rateLimit.perIpLimits) { - validateLimits(`${pathPrefix} perIpLimits`, rateLimit.perIpLimits, globalFillIntervalMs); - Object.entries(rateLimit.perIpLimits.overrides || {}).forEach(([name, override]) => - validateLimits( - `${pathPrefix} perIpLimits override '${name}'`, - override, - globalFillIntervalMs - ) + validatePerIpTokenBuckets( + `${pathPrefix} perIpLimits`, + rateLimit.perIpLimits, + globalFillIntervalMs ); } }); } -function validateEffectiveRateLimits( +export function validateEffectiveRateLimits( args: RateLimitEnvoyFilterArgs -): LocalLimits | undefined { +): LocalLimits { const collidingPathNames = Object.entries(args.rateLimits || {}) .filter(([, rl]) => reservedEntryKeys.includes(rl.name)) .map(([path]) => path); @@ -210,88 +293,151 @@ function validateEffectiveRateLimits( ); } - // Validate scan.yaml endpoint coverage - const scanEndpoints = parseScanYamlEndpoints(); + const effectiveRateLimits: LocalLimits = args.rateLimits || {}; - const configuredScanPrefixes = Object.keys(args.rateLimits || {}).filter(pathPrefix => - pathPrefix.startsWith('/api/scan') - ); + // the name is the descriptor value identifying the endpoint bucket, so two endpoints sharing + // one would share their buckets (and make envoy reject the duplicated descriptors) + const names = Object.values(effectiveRateLimits).map(rl => rl.name); + const duplicateNames = [...new Set(names.filter((name, i) => names.indexOf(name) !== i))]; + if (duplicateNames.length > 0) { + throw new Error( + `duplicate rate limit names: ${duplicateNames.join(', ')}; every endpoint needs its own name` + ); + } - const { missing, orphaned } = validateEndpointCoverage(scanEndpoints, configuredScanPrefixes); + if (args.protocol === 'grpc') { + validateGrpcPathPrefixes(effectiveRateLimits); + } - const tokenRegistryEndpoints = parseTokenRegistrySpecEndpoints(); + validateIpLimits('globalPerIpLimits', args.globalPerIpLimits); + Object.entries(effectiveRateLimits).forEach(([pathPrefix, rateLimit]) => { + validateIpLimits(pathPrefix, rateLimit.perIpLimits); + }); - const configuredRegistryPrefixes = Object.keys(args.rateLimits || {}).filter(pathPrefix => - pathPrefix.startsWith('/registry') - ); + validateTokenBuckets(args.globalLimits, effectiveRateLimits, args.globalPerIpLimits); - const registryValidation = validateEndpointCoverage( - tokenRegistryEndpoints, - configuredRegistryPrefixes - ); + return effectiveRateLimits; +} - const totalMissing = missing.concat(registryValidation.missing); - const totalOrphaned = orphaned.concat(registryValidation.orphaned); +export function clientIpDescriptorValue(ip: string): string { + return `${ip}/32`; +} - if (totalMissing.length > 0 || totalOrphaned.length > 0) { - const errorParts: string[] = ['Rate limit configuration errors:']; - if (totalMissing.length > 0) { - errorParts.push(`- Missing rate limit prefixes for endpoints: ${totalMissing.join(', ')}`); - errorParts.push( - "If you're adding new endpoints in a Splice PR, add them to cluster/configs/shared/rate-limits." - ); - } - if (totalOrphaned.length > 0) { - errorParts.push( - `- Orphaned rate limit prefixes not matching any schema route: ${totalOrphaned.join(', ')}` - ); - } - throw new Error(errorParts.join('\n')); - } +// We deliberately do not key on the raw x-forwarded-for header: clients can +// prepend arbitrary entries to it, and our gateway only appends to it, so +// the header value is attacker controlled and per-IP limits could be evaded +// by simply varying the header on every request. +// masked_remote_address instead uses the address envoy trusts, which for the +// sidecar is the last x-forwarded-for hop, i.e. the one appended by our own +// ingress gateway. +const maskedRemoteAddressAction = { + masked_remote_address: { + // one bucket per client address + v4_prefix_mask_len: 32, + // the default of 0 would put all IPv6 clients into a single bucket + v6_prefix_mask_len: 128, + }, +}; + +export function buildGlobalPerIpRateLimitAction(): unknown { + return { actions: [maskedRemoteAddressAction] }; +} - // Filter out banned and unlimited entries - const effectiveRateLimits = Object.fromEntries( - Object.entries(args.rateLimits || {}).filter( - (ent): ent is [string, LocalLimit] => { - // TODO (#4201): in banned case, implement actual banning with special short-circuit for whitelisted IPs - // Currently skipping banned endpoints instead of setting 0/0 limits - // in unlimited case, we fall back to globalRateLimit so don't need a rule - const [, rl] = ent; - return rl.type === 'limited'; - } - ) - ); +/** + * The per-client-IP descriptors for a set of per-IP limits, ordered from the most to the least + * specific one, as envoy consumes a token from the first matching descriptor only: the IP-specific + * overrides come first, the wildcard (one bucket per observed client IP) descriptor last. + * `baseEntries` are the descriptor entries the limits are scoped to, e.g. the per-endpoint + * `header_match` entry, or none at all for the global per-IP limits. + */ +function buildPerIpDescriptors( + baseEntries: { key: string; value?: string }[], + perIpLimits: PerIpLimits +): unknown[] { + const descriptors: unknown[] = []; - Object.entries(effectiveRateLimits).forEach(([pathPrefix, rateLimit]) => { - validateIpLimits(pathPrefix, rateLimit); + // IP-specific overrides first, so they take precedence over the generic per-IP bucket + Object.values(perIpLimits.overrides || {}).forEach(override => { + override.ips.forEach(ip => { + descriptors.push({ + entries: [...baseEntries, { key: clientIpEntryKey, value: clientIpDescriptorValue(ip) }], + token_bucket: tokenBucket(override), + }); + }); }); - validateTokenBuckets(args.globalLimits, effectiveRateLimits); + // Generic per-IP fallback last + descriptors.push({ + entries: [...baseEntries, { key: clientIpEntryKey }], + token_bucket: tokenBucket(perIpLimits), + }); - return effectiveRateLimits; + return descriptors; } -export function clientIpDescriptorValue(ip: string): string { - return `${ip}/32`; +/** + * The descriptors of the filter enforcing the global per-client-IP limit: the IP-specific overrides, + * followed by the wildcard bucket applying to every other client IP. + */ +export function buildGlobalPerIpRateLimitDescriptors(globalPerIpLimits: PerIpLimits): unknown[] { + return buildPerIpDescriptors([], globalPerIpLimits); +} + +function tokenBucket(limits: Limits): unknown { + return { + max_tokens: limits.maxTokens, + tokens_per_fill: limits.tokensPerFill, + fill_interval: limits.fillInterval, + }; +} + +function pathMatcher(pathPrefix: string, invertMatch: boolean): unknown { + return { + name: ':path', + string_match: { + prefix: pathPrefix, + ignore_case: true, + }, + // the matchers of a `header_value_match` action are ANDed, so an inverted one excludes + // the requests matching that prefix + ...(invertMatch ? { invert_match: true } : {}), + }; +} + +/** + * The configured path prefixes that are strictly more specific than `pathPrefix`, i.e. whose + * requests are a subset of the ones matched by `pathPrefix`. + */ +export function moreSpecificPathPrefixes(pathPrefix: string, allPathPrefixes: string[]): string[] { + return allPathPrefixes.filter( + other => + other.length > pathPrefix.length && + // envoy matches the prefixes case insensitively, so the comparison must too + other.toLowerCase().startsWith(pathPrefix.toLowerCase()) + ); } export function buildRateLimitActions(effectiveRateLimits: LocalLimits): unknown[] { + const allPathPrefixes = Object.keys(effectiveRateLimits); return Object.entries(effectiveRateLimits).flatMap(([pathPrefix, rateLimit]) => { const actions = []; - // Action 1: generate the per-endpoint action + // Action 1: generate the per-endpoint action. + // Configured prefixes may nest (e.g. `/registry/transfer-instruction/v1` and + // `/registry/transfer-instruction/v1/transfer-factory`). Without excluding the more specific + // prefixes, a request to the nested endpoint would generate *both* descriptors, and envoy + // consumes at most one descriptor bucket per filter, which would silently drop the limit + // configured for the more specific endpoint. Hence every endpoint matches exactly the + // requests that no more specific endpoint matches. const baseAction = { header_value_match: { descriptor_value: rateLimit.name, expect_match: true, headers: [ - { - name: ':path', - string_match: { - prefix: pathPrefix, - ignore_case: true, - }, - }, + pathMatcher(pathPrefix, false), + ...moreSpecificPathPrefixes(pathPrefix, allPathPrefixes).map(nested => + pathMatcher(nested, true) + ), ], }, }; @@ -301,24 +447,7 @@ export function buildRateLimitActions(effectiveRateLimits: LocalLimits ): unknown[] { - return Object.values(effectiveRateLimits).flatMap(rateLimit => { - const descs = []; - - // per-endpoint bucket - descs.push({ - entries: [{ key: 'header_match', value: rateLimit.name }], - token_bucket: { - max_tokens: rateLimit.maxTokens, - tokens_per_fill: rateLimit.tokensPerFill, - fill_interval: rateLimit.fillInterval, - }, - }); + return Object.values(effectiveRateLimits).map(rateLimit => ({ + entries: [{ key: 'header_match', value: rateLimit.name }], + token_bucket: tokenBucket(rateLimit), + })); +} - // generate the per-IP buckets if configured - if (rateLimit.perIpLimits) { - // IP-specific overrides first, so they take precedence over the generic per-IP bucket - Object.entries(rateLimit.perIpLimits.overrides || {}).forEach(([, override]) => { - override.ips.forEach(ip => { - descs.push({ - entries: [ - { key: 'header_match', value: rateLimit.name }, - { key: clientIpEntryKey, value: clientIpDescriptorValue(ip) }, - ], - token_bucket: { - max_tokens: override.maxTokens, - tokens_per_fill: override.tokensPerFill, - fill_interval: override.fillInterval, - }, - }); - }); - }); +/** + * The descriptors of the filter enforcing the per-endpoint, per-client-IP limits, ordered from the + * most to the least specific one, as envoy consumes a token from the first matching descriptor + * only. These are consumed on top of the global per-client-IP bucket, which is enforced by a + * separate filter. + */ +export function buildPerEndpointPerIpRateLimitDescriptors( + effectiveRateLimits: LocalLimits +): unknown[] { + return Object.values(effectiveRateLimits).flatMap(rateLimit => + rateLimit.perIpLimits + ? buildPerIpDescriptors( + [{ key: 'header_match', value: rateLimit.name }], + rateLimit.perIpLimits + ) + : [] + ); +} + +/** + * A bucket that never rate limits anything, used as the default token bucket of the filters whose + * limits are all expressed as descriptors. Envoy requires the default bucket to be set, but these + * filters must not limit requests that match none of their descriptors. + */ +function unlimitedTokenBucket(fillInterval: string): Limits { + return { + maxTokens: maxTokenValue, + tokensPerFill: maxTokenValue, + fillInterval, + }; +} + +/** + * A local rate limit filter: envoy consumes a token of its default bucket and/or of the first of + * its descriptors matching the request, independently of the other filters. + */ +export interface RateLimitFilter { + // the name of the filter in the HTTP filter chain, also the typed_per_filter_config key + name: string; + // the prefix of the metrics emitted by this filter + statPrefix: string; + // the value of the `x-local-rate-limit` response header set by this filter + limitName: string; + defaultTokenBucket: Limits; + alwaysConsumeDefaultTokenBucket: boolean; + descriptors: unknown[]; +} - // Generic per-IP fallback last - descs.push({ - entries: [{ key: 'header_match', value: rateLimit.name }, { key: clientIpEntryKey }], - token_bucket: { - max_tokens: rateLimit.perIpLimits.maxTokens, - tokens_per_fill: rateLimit.perIpLimits.tokensPerFill, - fill_interval: rateLimit.perIpLimits.fillInterval, +function localRateLimitFilterConfig(filter: RateLimitFilter, protocol: RateLimitProtocol): unknown { + return { + '@type': 'type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit', + stat_prefix: filter.statPrefix, + token_bucket: tokenBucket(filter.defaultTokenBucket), + // whether the default (non-descriptor) bucket is consumed by requests that matched one + // of the descriptors below + always_consume_default_token_bucket: filter.alwaysConsumeDefaultTokenBucket, + // A rate limited gRPC call is answered with HTTP 200 and a gRPC status; without this envoy + // reports the generic `UNAVAILABLE` (14), which clients cannot tell apart from a genuinely + // unavailable server (and typically retry). `RESOURCE_EXHAUSTED` (8) unambiguously identifies + // a rejection by the rate limiter, both for the client and in the sidecar access logs. + rate_limited_as_resource_exhausted: protocol === 'grpc', + filter_enabled: { + runtime_key: 'local_rate_limit_enabled', + default_value: { + numerator: 100, + denominator: 'HUNDRED', + }, + }, + filter_enforced: { + runtime_key: 'local_rate_limit_enforced', + default_value: { + numerator: 100, + denominator: 'HUNDRED', + }, + }, + response_headers_to_add: [ + { + append_action: 'OVERWRITE_IF_EXISTS_OR_ADD', + header: { + key: localRateLimitedHeader, + // identifies the limit that rejected the request in the sidecar access logs + value: filter.limitName, }, - }); - } + }, + ], + // Emit X-RateLimit-Limit/Remaining/Reset. + // used in sidecar access logs + // not sent to the client, because we strip them on the ingress gateway + enable_x_ratelimit_headers: 'DRAFT_VERSION_03', + max_dynamic_descriptors: maxDynamicDescriptorsPerLimit, + // Only the first matching descriptor is used, hence they are ordered from the most to the + // least specific one. Limits that must apply on top of each other live in separate filters. + descriptors: filter.descriptors, + }; +} - return descs; - }); +/** + * The local rate limit filters to install, one per limit that must be respected on its own, ordered + * from the most to the least specific limit. + * Every request consumes a token from the global bucket *and* a token from the bucket of its + * client IP, and, on the endpoints with configured limits, a token from the per-endpoint bucket + * and one from the per-endpoint bucket of its client IP. + * The filters run in the returned order, so a request rejected by a more specific limit never + * reaches (and never consumes the tokens of) the broader ones. + */ +export function buildRateLimitFilters( + globalLimits: Limits, + globalPerIpLimits: PerIpLimits, + effectiveRateLimits: LocalLimits +): RateLimitFilter[] { + const perEndpointPerIpDescriptors = + buildPerEndpointPerIpRateLimitDescriptors(effectiveRateLimits); + const perEndpointDescriptors = buildEndpointRateLimitDescriptors(effectiveRateLimits); + // a filter without descriptors would only ever consume its (unlimited) default bucket + const endpointFilters: RateLimitFilter[] = [ + // consumed on top of the global per-IP bucket, requests to endpoints without per-IP limits + // match no descriptor here and are not limited by this filter + ...(perEndpointPerIpDescriptors.length > 0 + ? [ + { + name: perEndpointPerIpRateLimitFilterName, + statPrefix: perEndpointPerIpRateLimitStatPrefix, + limitName: perEndpointPerIpRateLimitName, + defaultTokenBucket: unlimitedTokenBucket(globalLimits.fillInterval), + alwaysConsumeDefaultTokenBucket: false, + descriptors: perEndpointPerIpDescriptors, + }, + ] + : []), + // one bucket per configured endpoint; requests to any other endpoint match no descriptor + // here and are only limited globally + ...(perEndpointDescriptors.length > 0 + ? [ + { + name: perEndpointRateLimitFilterName, + statPrefix: perEndpointRateLimitStatPrefix, + limitName: perEndpointRateLimitName, + defaultTokenBucket: unlimitedTokenBucket(globalLimits.fillInterval), + alwaysConsumeDefaultTokenBucket: false, + descriptors: perEndpointDescriptors, + }, + ] + : []), + ]; + return [ + ...endpointFilters, + { + name: globalPerIpRateLimitFilterName, + statPrefix: globalPerIpRateLimitStatPrefix, + limitName: globalPerIpRateLimitName, + // every request matches one of the descriptors below, so this bucket is never used + defaultTokenBucket: unlimitedTokenBucket(globalLimits.fillInterval), + alwaysConsumeDefaultTokenBucket: false, + // one bucket per client IP (or per IP-specific override), consumed by every request + descriptors: buildGlobalPerIpRateLimitDescriptors(globalPerIpLimits), + }, + { + name: globalRateLimitFilterName, + statPrefix: globalRateLimitStatPrefix, + limitName: globalRateLimitName, + // the global limit applies to every request, and it is the only limit this filter enforces, + // so that a rejection is unambiguously attributed to it in the metrics and the access logs + defaultTokenBucket: globalLimits, + alwaysConsumeDefaultTokenBucket: true, + descriptors: [], + }, + ]; +} + +/** + * The per-route configuration of the local rate limit filters, keyed by filter name. + */ +export function buildTypedPerFilterConfig( + filters: RateLimitFilter[], + protocol: RateLimitProtocol = 'http' +): Record { + return Object.fromEntries( + filters.map(filter => [filter.name, localRateLimitFilterConfig(filter, protocol)]) + ); +} + +/** + * The `HTTP_FILTER` config patches adding the rate limit filters to the sidecar's HTTP filter + * chain. Each of them consumes a token of its own buckets, so that a single request consumes a + * global token, a token of the bucket of its client IP, and the per-endpoint tokens where + * configured. + * The insertion point is pinned to the router filter, which is always the last filter of the + * chain: istio applies the patches in order, each of them inserting right before the router, so + * the filters end up in the chain in the order of `buildRateLimitFilters`, i.e. from the most to + * the least specific limit. Without the `subFilter` match istio would instead insert every filter + * before the *first* filter of the chain, silently reversing them. + * The patches are scoped to `inboundPort`, the port carrying the externally reachable API. A + * workload typically listens on further ports that are not exposed to the outside (e.g. the + * sequencer's admin and peer-to-peer ports, which istio also treats as HTTP because they are + * named `grpc-*`); without the port match the filters would be inserted into the filter chains of + * those listeners too, and internal traffic must never be rate limited. + */ +export function buildHttpFilterPatches( + filters: RateLimitFilter[], + inboundPort: pulumi.Input +): unknown[] { + return filters.map(filter => ({ + applyTo: 'HTTP_FILTER', + match: { + context: 'SIDECAR_INBOUND', + listener: { + portNumber: inboundPort, + filterChain: { + filter: { + name: 'envoy.filters.network.http_connection_manager', + subFilter: { + name: 'envoy.filters.http.router', + }, + }, + }, + }, + }, + patch: { + operation: 'INSERT_BEFORE', + value: { + name: filter.name, + typed_config: { + '@type': 'type.googleapis.com/udpa.type.v1.TypedStruct', + type_url: + 'type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit', + value: { + // the filters are disabled by default and only enabled (and configured) + // on the routes via typed_per_filter_config below + stat_prefix: filter.statPrefix, + }, + }, + }, + }, + })); } export class RateLimitEnvoyFilter extends pulumi.ComponentResource { @@ -387,7 +716,16 @@ export class RateLimitEnvoyFilter extends pulumi.ComponentResource { super('splice:RateLimit', `splice-${args.namespace}-${name}`, args, opts); const effectiveRateLimits = validateEffectiveRateLimits(args); - const rateLimitActions = buildRateLimitActions(effectiveRateLimits || {}); + // The global per-IP action comes last so that the more specific per-endpoint + // descriptors are matched first. + const rateLimitActions = buildRateLimitActions(effectiveRateLimits).concat([ + buildGlobalPerIpRateLimitAction(), + ]); + const rateLimitFilters = buildRateLimitFilters( + args.globalLimits, + args.globalPerIpLimits, + effectiveRateLimits + ); this.envoyFilter = new k8s.apiextensions.CustomResource( `${args.namespace}-${name}`, @@ -405,35 +743,8 @@ export class RateLimitEnvoyFilter extends pulumi.ComponentResource { }, }, configPatches: [ - // Patch 1: Add the rate limit filter to the HTTP filter chain. - { - applyTo: 'HTTP_FILTER', - match: { - context: 'SIDECAR_INBOUND', - listener: { - filterChain: { - filter: { - name: 'envoy.filters.network.http_connection_manager', - }, - }, - }, - }, - patch: { - operation: 'INSERT_BEFORE', - value: { - name: 'envoy.filters.http.local_ratelimit', - typed_config: { - '@type': 'type.googleapis.com/udpa.type.v1.TypedStruct', - type_url: - 'type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit', - value: { - stat_prefix: 'http_local_rate_limiter', - }, - }, - }, - }, - }, - // Patch 2: Configure the rate limiting rules on the HTTP route. + ...buildHttpFilterPatches(rateLimitFilters, args.inboundPort), + // Configure the rate limiting rules on the HTTP route. { applyTo: 'HTTP_ROUTE', match: { @@ -449,52 +760,14 @@ export class RateLimitEnvoyFilter extends pulumi.ComponentResource { operation: 'MERGE', value: { route: { + // shared by all the filters: the descriptors generated here are matched + // against the descriptors of each filter, which are disjoint rate_limits: rateLimitActions, }, - typed_per_filter_config: { - 'envoy.filters.http.local_ratelimit': { - '@type': - 'type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit', - stat_prefix: 'http_local_rate_limiter', - token_bucket: { - max_tokens: args.globalLimits.maxTokens, - tokens_per_fill: args.globalLimits.tokensPerFill, - fill_interval: args.globalLimits.fillInterval, - }, - filter_enabled: { - runtime_key: 'local_rate_limit_enabled', - default_value: { - numerator: 100, - denominator: 'HUNDRED', - }, - }, - filter_enforced: { - runtime_key: 'local_rate_limit_enforced', - default_value: { - numerator: 100, - denominator: 'HUNDRED', - }, - }, - response_headers_to_add: [ - { - append_action: 'OVERWRITE_IF_EXISTS_OR_ADD', - header: { - key: localRateLimitedHeader, - value: 'true', - }, - }, - ], - // Emit X-RateLimit-Limit/Remaining/Reset. - // used in sidecar access logs - // not sent to the client, because we strip them on the ingress gateway - enable_x_ratelimit_headers: 'DRAFT_VERSION_03', - max_dynamic_descriptors: maxDynamicDescriptorsPerLimit, - // simplified descriptors by combining with actions and requiring all the tokens of an action to be set - // a descriptor in practice is a subset of tags from a rate limit - // but important to note that for each rate limit only one descriptor can match, if multiple descriptors match, the first one is used - descriptors: buildRateLimitDescriptors(effectiveRateLimits || {}), - }, - }, + typed_per_filter_config: buildTypedPerFilterConfig( + rateLimitFilters, + args.protocol || 'http' + ), }, }, }, diff --git a/cluster/pulumi/common/src/ratelimit/index.ts b/cluster/pulumi/common/src/ratelimit/index.ts index fe638079e0c..0534f5e3a8f 100644 --- a/cluster/pulumi/common/src/ratelimit/index.ts +++ b/cluster/pulumi/common/src/ratelimit/index.ts @@ -3,3 +3,4 @@ export * from './rateLimitSchema'; export * from './rateLimitHeaders'; export * from './envoyRateLimiter'; +export * from './rateLimit'; diff --git a/cluster/pulumi/common/src/ratelimit/rateLimit.test.ts b/cluster/pulumi/common/src/ratelimit/rateLimit.test.ts new file mode 100644 index 00000000000..85b0afa259e --- /dev/null +++ b/cluster/pulumi/common/src/ratelimit/rateLimit.test.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect, test } from '@jest/globals'; + +import { rateLimitedRequestsExpression } from './rateLimit'; + +test('the access log filter matches the rejections of the configured protocol', () => { + // a rate limited HTTP request is answered with 429 + expect(rateLimitedRequestsExpression('http')).toEqual('response.code == 429'); + // whereas a rate limited gRPC call (e.g. on the sequencer's public API) is answered with + // HTTP 200 and the RESOURCE_EXHAUSTED (8) gRPC status, so filtering on the HTTP status code + // would never log it + expect(rateLimitedRequestsExpression('grpc')).toEqual('response.grpc_status == 8'); +}); diff --git a/cluster/pulumi/common/src/ratelimit/rateLimit.ts b/cluster/pulumi/common/src/ratelimit/rateLimit.ts index fe1e9f2f575..190a6a8db94 100644 --- a/cluster/pulumi/common/src/ratelimit/rateLimit.ts +++ b/cluster/pulumi/common/src/ratelimit/rateLimit.ts @@ -2,14 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 import * as k8s from '@pulumi/kubernetes'; -import { RateLimitEnvoyFilter } from './envoyRateLimiter'; +import { RateLimitProtocol, RateLimitEnvoyFilter, rateLimitedGrpcStatus } from './envoyRateLimiter'; import { ExternalRateLimit } from './rateLimitSchema'; +/** + * Envoy answers a rate limited HTTP request with `429`, but a rate limited gRPC call with HTTP + * `200` and the `RESOURCE_EXHAUSTED` gRPC status (see `rate_limited_as_resource_exhausted`), so + * filtering on the HTTP status code alone would never match a gRPC rejection. + */ +export function rateLimitedRequestsExpression(protocol: RateLimitProtocol): string { + return protocol === 'grpc' + ? `response.grpc_status == ${rateLimitedGrpcStatus}` + : 'response.code == 429'; +} + /** * Makes the sidecar log every rate limited request, independently of whether access * logging is enabled cluster wide. */ -function logRateLimitedRequests(namespace: string, app: string): k8s.apiextensions.CustomResource { +function logRateLimitedRequests( + namespace: string, + app: string, + protocol: RateLimitProtocol +): k8s.apiextensions.CustomResource { return new k8s.apiextensions.CustomResource(`${namespace}-${app}-rate-limit-access-log`, { apiVersion: 'telemetry.istio.io/v1', kind: 'Telemetry', @@ -29,25 +44,50 @@ function logRateLimitedRequests(namespace: string, app: string): k8s.apiextensio providers: [{ name: 'envoy' }], // Rate limited requests are recognizable in the log by // response_code_details=local_rate_limited and response_flags containing RL. - filter: { expression: 'response.code == 429' }, + filter: { expression: rateLimitedRequestsExpression(protocol) }, }, ], }, }); } +export interface RateLimitOptions { + protocol?: RateLimitProtocol; +} + export function installRateLimits( namespace: string, app: string, appPort: number, - rateLimit: ExternalRateLimit + rateLimit: ExternalRateLimit, + options: RateLimitOptions = {} ): void { + const protocol = options.protocol || 'http'; new RateLimitEnvoyFilter(`${app}-rate-limit`, { namespace: namespace, appLabel: app, inboundPort: appPort, globalLimits: rateLimit.globalLimits, + globalPerIpLimits: rateLimit.globalPerIpLimits, rateLimits: rateLimit.rateLimits, + protocol, + }); + logRateLimitedRequests(namespace, app, protocol); +} + +// The sequencer's public API port (`grpc-cs-pub-api`), the only externally reachable sequencer +// port that carries client traffic. The admin (5009) and peer-to-peer (5010) ports must not be +// rate limited: they serve operator and inter-sequencer traffic, which would break under a limit +// sized for clients. +export const sequencerPublicApiPort = 5008; + +export function installSequencerRateLimits( + namespace: string, + sequencerApp: string, + rateLimit: ExternalRateLimit +): void { + // the sequencer's public API is gRPC + installRateLimits(namespace, sequencerApp, sequencerPublicApiPort, rateLimit, { + protocol: 'grpc', }); - logRateLimitedRequests(namespace, app); } diff --git a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts index 2ca5ae6fdb0..bc71d48d3f2 100644 --- a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts +++ b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.test.ts @@ -30,6 +30,69 @@ test('RateLimitSchema accepts config without overrides', () => { expect(() => RateLimitSchema.parse(validConfig)).not.toThrow(); }); +test('RateLimitSchema defaults to global and global per-IP limits', () => { + const parsed = RateLimitSchema.parse({}); + expect(parsed).toEqual({ + globalLimits: { maxTokens: 10000, tokensPerFill: 10000, fillInterval: '60s' }, + globalPerIpLimits: { maxTokens: 1000, tokensPerFill: 1000, fillInterval: '60s' }, + }); +}); + +test('RateLimitSchema keeps per-endpoint limits optional', () => { + const parsed = RateLimitSchema.parse(validConfig); + expect(parsed.rateLimits).toBeDefined(); +}); + +test('RateLimitSchema accepts named overrides on the global per-IP limits', () => { + const config = { + ...validConfig, + globalPerIpLimits: { + maxTokens: 1000, + tokensPerFill: 1000, + fillInterval: '60s', + overrides: { + 'multi-validators': { + ips: ['192.68.78.51', '192.68.78.52'], + maxTokens: 5000, + tokensPerFill: 5000, + fillInterval: '60s', + }, + }, + }, + }; + expect(() => RateLimitSchema.parse(config)).not.toThrow(); +}); + +test('RateLimitSchema rejects non-IPv4 addresses in global per-IP overrides', () => { + const config = { + ...validConfig, + globalPerIpLimits: { + maxTokens: 1000, + tokensPerFill: 1000, + fillInterval: '60s', + overrides: { + 'multi-validators': { + ips: ['2001:db8::1'], + maxTokens: 5000, + tokensPerFill: 5000, + fillInterval: '60s', + }, + }, + }, + }; + expect(() => RateLimitSchema.parse(config)).toThrow(); +}); + +test('RateLimitSchema rejects an unknown endpoint type', () => { + const config = { + ...validConfig, + rateLimits: { + '/api/scan/livez': { name: 'livez', type: 'whatever' }, + }, + }; + expect(() => RateLimitSchema.parse(config)).toThrow(); +}); + test('RateLimitSchema accepts named overrides with ips', () => { const config = { ...validConfig, diff --git a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts index a08c3e78d31..807818d7c36 100644 --- a/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts +++ b/cluster/pulumi/common/src/ratelimit/rateLimitSchema.ts @@ -21,35 +21,37 @@ export const PerIpLimitsSchema = BucketRateLimitSchema.extend({ overrides: z.record(z.string().min(1), OverrideSchema).optional(), }); -const BucketMatchedRateLimitSchema = BucketRateLimitSchema.extend({ +export const RateLimitConfigSchema = BucketRateLimitSchema.extend({ type: z.literal('limited'), perIpLimits: PerIpLimitsSchema.optional(), }); -export const BannedSchema = z.object({ - type: z.literal('banned'), -}); - -export const UnlimitedSchema = z.object({ - type: z.literal('unlimited'), -}); +export type ExternalRateLimit = z.infer; -export const RateLimitConfigSchema = z.discriminatedUnion('type', [ - BucketMatchedRateLimitSchema, - BannedSchema, - UnlimitedSchema, -]); +export const defaultGlobalLimits = { + maxTokens: 10000, + tokensPerFill: 10000, + fillInterval: '60s', +}; -export type ExternalRateLimit = z.infer; +export const defaultGlobalPerIpLimits = { + maxTokens: 1000, + tokensPerFill: 1000, + fillInterval: '60s', +}; export const RateLimitSchema = z.object({ - globalLimits: BucketRateLimitSchema, - rateLimits: z.object({}).catchall( - z.intersection( - z.object({ - name: z.string(), - }), - RateLimitConfigSchema + globalLimits: BucketRateLimitSchema.default(defaultGlobalLimits), + globalPerIpLimits: PerIpLimitsSchema.default(defaultGlobalPerIpLimits), + rateLimits: z + .object({}) + .catchall( + z.intersection( + z.object({ + name: z.string(), + }), + RateLimitConfigSchema + ) ) - ), + .optional(), }); diff --git a/cluster/pulumi/gcp/src/gcpProject.ts b/cluster/pulumi/gcp/src/gcpProject.ts index 24a4e4581da..c85052b597f 100644 --- a/cluster/pulumi/gcp/src/gcpProject.ts +++ b/cluster/pulumi/gcp/src/gcpProject.ts @@ -72,6 +72,18 @@ class GcpProject extends pulumi.ComponentResource { opts ); + // Cloud Armor is billed per project via a tier. We only use features available in + // the Standard tier (IP whitelisting, rate limiting, preconfigured WAF rules), so + // pin the project to it rather than an Enterprise tier. + new gcp.compute.ProjectCloudArmorTier( + 'cloud-armor-tier', + { + project: gcpProjectId, + cloudArmorTier: 'CA_STANDARD', + }, + opts + ); + // Source SV identities from a pre-existing project (i.e. devnet) // Note: this should be fine when ran against devnet itself... // - But since we can automate this now, we might want to simply generate new SV secrets per project diff --git a/cluster/pulumi/infra/src/cloudArmor.ts b/cluster/pulumi/infra/src/cloudArmor.ts index fdccf6fe3d6..17eba4b5f65 100644 --- a/cluster/pulumi/infra/src/cloudArmor.ts +++ b/cluster/pulumi/infra/src/cloudArmor.ts @@ -3,14 +3,22 @@ import * as gcp from '@pulumi/gcp'; import * as pulumi from '@pulumi/pulumi'; import * as _ from 'lodash'; -import { CLUSTER_BASENAME, extractPathPrefixes } from '@canton-network/splice-pulumi-common'; +import { CLUSTER_BASENAME, getDnsNames } from '@canton-network/splice-pulumi-common'; import { PerEndpointLimits } from '@canton-network/splice-pulumi-common/src/ratelimit/envoyRateLimiter'; import * as config from './config'; +import { + allowedPathsCondition, + hostCondition, + ipWhitelistRuleChunks, + matchExpression, +} from './cloudArmorRules'; +import { loadIPRanges } from './whitelisting/ipRanges'; // Rule number ranges -const THROTTLE_BAN_RULE_MIN = 100000000; -const THROTTLE_BAN_RULE_MAX = 200000000; +const IP_WHITELIST_RULE_MIN = 1000010; +const THROTTLE_BAN_RULE_MIN = 100000010; +const THROTTLE_BAN_RULE_MAX = 200000010; const DEFAULT_DENY_RULE_NUMBER = 2147483647; const RULE_SPACING = 100; @@ -69,7 +77,19 @@ export function configureCloudArmorPolicy( // but the preview would entail changing this array if the rules were changed, // making those changes harder to review than with the separate resources }, - opts + { + ...opts, + // All rules are managed as separate PolicyRule resources below, which use the + // per-rule addRule/patchRule/removeRule endpoints. The policy resource itself + // still reads the full rule list back from GCP, so a refresh imports the rules + // into this resource's inputs and Pulumi then wants to "update" the policy. + // That update is a PATCH on the parent policy which always carries the whole + // rule list, and GCP rejects it once the policy is attached to a load balancer + // backend and contains a deny rule: + // Error 400: HTTP references are not supported for security policies with deny rules. + // Ignoring `rules` here keeps the parent policy free of spurious updates. + ignoreChanges: [...(opts?.ignoreChanges ?? []), 'rules'], + } ); const ruleOpts = { ...opts, parent: securityPolicy, deletedWith: securityPolicy }; @@ -81,7 +101,7 @@ export function configureCloudArmorPolicy( } // Step 3: Add IP whitelisting rules - addIpWhitelistRules(/*securityPolicy, cac.allRulesPreviewOnly, ruleOpts*/); + addIpWhitelistRules(securityPolicy, cac.allRulesPreviewOnly, ruleOpts); // Step 4: Add throttling/banning rules for specific API endpoints if (cac.publicEndpoints && !_.isEmpty(cac.publicEndpoints)) { @@ -114,19 +134,58 @@ function addPredefinedWafRules(): void { } /** - * Adds IP whitelisting rules to a security policy + * Allows the internal and SV whitelisted source IPs to reach any host and path. + * + * These rules sit at a lower priority number than the public endpoint rules, so they + * are evaluated first: whitelisted traffic is allowed outright and never throttled. + * + * Without them, everything served through the L7 gateway that is not listed in + * `publicEndpoints` (grafana, prometheus, alertmanager, the SV and wallet UIs, docs, + * /version, ...) would be dropped by the default deny rule. Cloud Armor runs in front + * of istio and knows nothing about the istio AuthorizationPolicy whitelisting, which + * still applies afterwards for the fine grained per-host rules. */ -function addIpWhitelistRules(): void { - /* - securityPolicy: Policy, +function addIpWhitelistRules( + securityPolicy: CloudArmorPolicy, preview: boolean, opts: pulumi.ResourceOptions - */ - // TODO (DACH-NY/canton-network-internal#1250) implement +): void { + // only the internal and SV whitelists, not the full set of external ranges + loadIPRanges(true).apply(ranges => { + const chunks = ipWhitelistRuleChunks(ranges, THROTTLE_BAN_RULE_MIN - IP_WHITELIST_RULE_MIN); + + return chunks.map( + (chunk, i) => + new PolicyRule( + `ip-whitelist-${i}`, + { + securityPolicy: securityPolicy.name, + region: securityPolicy.region, + description: `Allow whitelisted source IPs (${i + 1} of ${chunks.length})`, + priority: IP_WHITELIST_RULE_MIN + i, + preview, + action: 'allow', + match: { + versionedExpr: 'SRC_IPS_V1', + config: { + srcIpRanges: chunk, + }, + }, + }, + opts + ) + ); + }); } /** - * Adds throttle and ban rules for API endpoints to a security policy + * Adds allow/throttle rules for the publicly reachable API endpoints. Any endpoint + * without a matching rule here is blocked by the default deny rule. + * + * Throttling is enforced per source IP. Cloud Armor only ever applies one rate limit + * rule per request (first match by priority wins, and conforming requests are allowed + * outright), so a global cap cannot be stacked on top of these rules; it is enforced by + * envoy instead. */ function addThrottleAndBanRules( securityPolicy: CloudArmorPolicy, @@ -143,41 +202,65 @@ function addThrottleAndBanRules( ); } - const { hostname, pathPrefix, throttleAcrossAllEndpointsAllIps } = singleServiceThrottle; + const { + hostname, + hostPrefixRegex, + pathPrefix, + restrictToRateLimitedPaths, + throttleAcrossAllEndpointsPerIp, + } = singleServiceThrottle; + const throttled = throttleAcrossAllEndpointsPerIp !== undefined; // leave out the rule but consume the priority number if max is 0 // this makes the pulumi update cleaner if toggling just one service - if (throttleAcrossAllEndpointsAllIps.maxRequestsBeforeHttp429 > 0) { - const ruleName = `throttle-all-endpoints-all-ips-${confEntryHead}`; - const hostnameRegex = hostname - ? _.escapeRegExp(hostname) - : `scan\\.[\\w-]+\\.${_.escapeRegExp(config.clusterHostname)}`; - const pathExpr = allowedPathsCondition(scanExternalRateLimits, pathPrefix); - const hostExpr = `request.headers['host'].matches(R"^${hostnameRegex}(?::[0-9]+)?$")`; - const matchExpr = `${pathExpr} && ${hostExpr}`; + const skipRule = throttled && throttleAcrossAllEndpointsPerIp.maxRequestsBeforeHttp429 === 0; + + if (!skipRule) { + const ruleName = throttled + ? `throttle-all-endpoints-per-ip-${confEntryHead}` + : `allow-all-endpoints-all-ips-${confEntryHead}`; + const pathExpr = allowedPathsCondition( + confEntryHead, + scanExternalRateLimits, + pathPrefix, + restrictToRateLimitedPaths + ); + const hostExpr = hostCondition( + confEntryHead, + [getDnsNames().cantonDnsName, getDnsNames().daDnsName], + hostname, + hostPrefixRegex + ); + const matchExpr = matchExpression(confEntryHead, pathExpr, hostExpr); new PolicyRule( ruleName, { securityPolicy: securityPolicy.name, region: securityPolicy.region, - description: `Throttle rule for all ${confEntryHead} API endpoints`, + description: throttled + ? `Per source IP throttle rule for all ${confEntryHead} API endpoints` + : `Allow rule for all ${confEntryHead} API endpoints`, priority, preview: preview || singleServiceThrottle.rulePreviewOnly, - action: 'throttle', + action: throttled ? 'throttle' : 'allow', match: { expr: { expression: matchExpr, }, }, - rateLimitOptions: { - enforceOnKey: 'ALL', - rateLimitThreshold: { - count: throttleAcrossAllEndpointsAllIps.maxRequestsBeforeHttp429, - intervalSec: throttleAcrossAllEndpointsAllIps.withinIntervalSeconds, - }, - conformAction: 'allow', - exceedAction: 'deny(429)', // 429 Too Many Requests - }, + ...(throttled + ? { + rateLimitOptions: { + enforceOnKey: 'IP', + rateLimitThreshold: { + count: throttleAcrossAllEndpointsPerIp.maxRequestsBeforeHttp429, + intervalSec: throttleAcrossAllEndpointsPerIp.withinIntervalSeconds, + }, + conformAction: 'allow', + exceedAction: 'deny(429)', // 429 Too Many Requests + }, + } + : {}), }, opts ); @@ -196,22 +279,25 @@ function addDefaultDenyRule( preview: boolean, opts: pulumi.ResourceOptions ): void { - // when you create a SecurityPolicy it has a default allow rule; we assume - // that if you want all rules in preview, you *also* still want to allow - // all traffic - if (preview) { - return; - } + // The default rule is created together with the policy and cannot be added or + // removed, only patched (the GCP provider turns a create at this priority into a + // patch, and skips the delete). So we always declare it: dropping the resource when + // `preview` is set would leave whatever action was last applied in place, i.e. a + // previously enforced deny would silently keep denying. new PolicyRule( 'default-deny', { securityPolicy: securityPolicy.name, region: securityPolicy.region, - description: 'Default rule to deny all other traffic', + description: preview + ? 'Default rule allowing all other traffic, as all rules are in preview mode' + : 'Default rule to deny all other traffic', priority: DEFAULT_DENY_RULE_NUMBER, - // default rule cannot be in preview mode; google API gives 400 if you try + // default rule cannot be in preview mode; google API gives 400 if you try. + // we assume that if you want all rules in preview, you *also* still want to + // allow all traffic. preview: false, - action: 'deny', + action: preview ? 'allow' : 'deny', match: { versionedExpr: 'SRC_IPS_V1', config: { @@ -222,40 +308,3 @@ function addDefaultDenyRule( opts ); } - -// Extract un-banned path prefixes and build optimized regex -function allowedPathsCondition(scanExternalRateLimits: PerEndpointLimits, pathPrefix: string) { - if (scanExternalRateLimits.rateLimits && !_.isEmpty(scanExternalRateLimits.rateLimits)) { - const pathPrefixes = extractPathPrefixes(scanExternalRateLimits.rateLimits) - .filter(p => !p.isBanned) - .map(p => p.pathPrefix); - - const basePrefix = pathPrefix.endsWith('/') ? pathPrefix : `${pathPrefix}/`; - const dynamicPathRxs = pathPrefixes - .filter(p => p.startsWith(basePrefix)) - .map(p => _.escapeRegExp(p.substring(basePrefix.length))); - - // Build regex pattern - if (dynamicPathRxs.length > 0) { - const regexPattern = `${basePrefix}(?:${dynamicPathRxs.join('|')})`; - const pathExpr = `request.path.matches(R"^${regexPattern}")`; - - // limit from https://docs.cloud.google.com/armor/quotas#limits - // in which a "subexpression" is an arg to && or || - if (pathExpr.length > 1024) { - throw new Error( - `Cloud Armor path expression exceeds 1024 character limit (current: ${pathExpr.length}). ` + - `Consider grouping path prefixes more aggressively.` - ); - } - - return pathExpr; - } else { - // Fallback to simple prefix matching if no paths were resolved - return `request.path.startsWith(R"${pathPrefix}")`; - } - } else { - // No rate limits specified, use simple prefix matching - return `request.path.startsWith(R"${pathPrefix}")`; - } -} diff --git a/cluster/pulumi/infra/src/cloudArmorRules.test.ts b/cluster/pulumi/infra/src/cloudArmorRules.test.ts new file mode 100644 index 00000000000..833f2968aa1 --- /dev/null +++ b/cluster/pulumi/infra/src/cloudArmorRules.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect, test, describe } from '@jest/globals'; + +import { + allowedPathsCondition, + hostCondition, + ipWhitelistRuleChunks, + matchExpression, + MAX_IPS_PER_RULE, + MAX_IP_WHITELIST_RULES, + MAX_SUBEXPRESSION_LENGTH, +} from './cloudArmorRules'; + +const dnsNames = [ + 'scratchd.network.canton.global', + 'scratchd.global.canton.network.digitalasset.com', +]; + +const scanRateLimits = { + rateLimits: { + '/api/scan/v0/dso': { name: 'dso', type: 'limited' }, + '/registry/metadata/v1/info': { name: 'info', type: 'limited' }, + // not externally reachable, so extractPathPrefixes drops it + '/api/sv/v0/dso': { name: 'sv-dso', type: 'limited' }, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as any; + +// mirrors how Cloud Armor evaluates `matches`: an unanchored RE2 search on the +// normalized value, with our expressions anchoring explicitly via ^ +function matchesFromExpr(expr: string): (value: string) => boolean { + const asMatches = + /^request\.(?:headers\['host'\]|path)\.lower\(\)(?:\.urlDecode\(\))?\.matches\(R"(.*)"\)$/.exec( + expr + ); + if (asMatches) { + const re = new RegExp(asMatches[1]); + return value => re.test(value.toLowerCase()); + } + const asStartsWith = /^request\.path\.lower\(\)\.urlDecode\(\)\.startsWith\(R"(.*)"\)$/.exec( + expr + ); + if (asStartsWith) { + return value => value.toLowerCase().startsWith(asStartsWith[1]); + } + throw new Error(`unrecognized expression: ${expr}`); +} + +describe('hostCondition', () => { + test('matches per-node hosts on every cluster DNS name, with and without a port', () => { + const expr = hostCondition('publicScan', dnsNames, undefined, 'scan')!; + const matches = matchesFromExpr(expr); + + expect(matches('scan.sv-2.scratchd.network.canton.global')).toBe(true); + expect(matches('scan.sv-2.scratchd.global.canton.network.digitalasset.com')).toBe(true); + expect(matches('scan.sv-2.scratchd.network.canton.global:443')).toBe(true); + expect(matches('SCAN.sv-2.scratchd.network.canton.global')).toBe(true); + + expect(matches('sv.sv-2.scratchd.network.canton.global')).toBe(false); + expect(matches('scan.sv-2.scratcha.network.canton.global')).toBe(false); + // the escaped dots must not act as wildcards + expect(matches('scan.sv-2.scratchdXnetwork.canton.global')).toBe(false); + // must be fully anchored + expect(matches('evil.scan.sv-2.scratchd.network.canton.global')).toBe(false); + expect(matches('scan.sv-2.scratchd.network.canton.global.evil.com')).toBe(false); + }); + + test('sequencer prefix regex matches all migration ids, but not the P2P API', () => { + const matches = matchesFromExpr( + hostCondition('sequencer', dnsNames, undefined, 'sequencer-[0-9]+')! + ); + + expect(matches('sequencer-0.sv-1.scratchd.network.canton.global')).toBe(true); + expect(matches('sequencer-12.sv-1.scratchd.network.canton.global')).toBe(true); + expect(matches('sequencer-0.sv-1.scratchd.global.canton.network.digitalasset.com:443')).toBe( + true + ); + + // the P2P API has no rule of its own: peer SVs are covered by the IP whitelist + expect(matches('sequencer-p2p-3.sv-1.scratchd.network.canton.global')).toBe(false); + expect(matches('sequencer.sv-1.scratchd.network.canton.global')).toBe(false); + expect(matches('scan.sv-1.scratchd.network.canton.global')).toBe(false); + }); + + test('exact hostname is anchored and regex-escaped', () => { + const matches = matchesFromExpr( + hostCondition('publicScan', dnsNames, 'scan.sv-2.scratchd.network.canton.global')! + ); + expect(matches('scan.sv-2.scratchd.network.canton.global')).toBe(true); + expect(matches('scan.sv-2.scratchdXnetwork.canton.global')).toBe(false); + expect(matches('scan.sv-3.scratchd.network.canton.global')).toBe(false); + }); + + test('is undefined when neither hostname nor prefix is given', () => { + expect(hostCondition('anything', dnsNames)).toBeUndefined(); + }); +}); + +describe('allowedPathsCondition', () => { + test('narrows to the rate limited paths under the prefix', () => { + const matches = matchesFromExpr( + allowedPathsCondition('publicScan', scanRateLimits, '/api/scan', true) + ); + + expect(matches('/api/scan/v0/dso')).toBe(true); + expect(matches('/api/scan/v0/dso/extra')).toBe(true); + expect(matches('/api/scan/v0/not-rate-limited')).toBe(false); + expect(matches('/registry/metadata/v1/info')).toBe(false); + }); + + test('narrows the token registry to its own paths', () => { + const matches = matchesFromExpr( + allowedPathsCondition('tokenRegistry', scanRateLimits, '/registry', true) + ); + expect(matches('/registry/metadata/v1/info')).toBe(true); + expect(matches('/api/scan/v0/dso')).toBe(false); + }); + + test('does not narrow when restrictToRateLimitedPaths is false', () => { + // regression: the scan/registry rate limit prefixes all start with "/", so + // narrowing a "/" prefix used to produce a scan-only regex that matched none of + // the sequencer's gRPC paths, silently denying all sequencer traffic + const matches = matchesFromExpr(allowedPathsCondition('sequencer', scanRateLimits, '/', false)); + + expect(matches('/com.digitalasset.canton.sequencer.api.v30.SequencerService/Subscribe')).toBe( + true + ); + expect(matches('/api/scan/v0/dso')).toBe(true); + }); + + test('falls back to a prefix match when no rate limited path is under the prefix', () => { + const matches = matchesFromExpr( + allowedPathsCondition('other', scanRateLimits, '/api/sv', true) + ); + expect(matches('/api/sv/v0/dso')).toBe(true); + expect(matches('/api/scan/v0/dso')).toBe(false); + }); + + test('throws when the generated path expression is too long', () => { + const manyPaths = Object.fromEntries( + Array.from({ length: 200 }, (_unused, i) => [ + `/api/scan/v0/endpoint-with-a-fairly-long-name-${i}`, + { name: `e${i}`, type: 'limited' }, + ]) + ); + expect(() => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + allowedPathsCondition('publicScan', { rateLimits: manyPaths } as any, '/api/scan', true) + ).toThrow(new RegExp(`exceeds the ${MAX_SUBEXPRESSION_LENGTH} character limit`)); + }); +}); + +describe('matchExpression', () => { + test('combines path and host conditions', () => { + expect(matchExpression('publicScan', 'PATH', 'HOST')).toBe('PATH && HOST'); + }); + + test('omits the host condition when there is none', () => { + expect(matchExpression('publicScan', 'PATH', undefined)).toBe('PATH'); + }); +}); + +describe('ipWhitelistRuleChunks', () => { + const ips = (n: number) => Array.from({ length: n }, (_unused, i) => `10.0.${i}.0/24`); + + test('respects the per-rule IP range limit', () => { + const chunks = ipWhitelistRuleChunks(ips(25), 100); + expect(chunks).toHaveLength(3); + expect(chunks[0]).toHaveLength(MAX_IPS_PER_RULE); + expect(chunks[2]).toHaveLength(5); + expect(chunks.flat().sort()).toEqual(ips(25).sort()); + }); + + test('deduplicates and sorts so rule contents are stable across runs', () => { + expect(ipWhitelistRuleChunks(['10.0.2.0/24', '10.0.1.0/24', '10.0.2.0/24'], 100)).toEqual([ + ['10.0.1.0/24', '10.0.2.0/24'], + ]); + }); + + test('refuses an empty whitelist rather than locking everything out', () => { + expect(() => ipWhitelistRuleChunks([], 100)).toThrow(/every internal endpoint would be denied/); + }); + + test('refuses to exceed the per-policy rule budget', () => { + expect(() => + ipWhitelistRuleChunks(ips(MAX_IP_WHITELIST_RULES * MAX_IPS_PER_RULE + 1), 100000) + ).toThrow(/exceeds the 150 rule budget/); + }); + + test('refuses to overlap the throttle rule priority range', () => { + expect(() => ipWhitelistRuleChunks(ips(50), 2)).toThrow( + /would overlap the throttle rule priority range/ + ); + }); +}); diff --git a/cluster/pulumi/infra/src/cloudArmorRules.ts b/cluster/pulumi/infra/src/cloudArmorRules.ts new file mode 100644 index 00000000000..202e8869133 --- /dev/null +++ b/cluster/pulumi/infra/src/cloudArmorRules.ts @@ -0,0 +1,161 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as _ from 'lodash'; +import { + extractPathPrefixes, + PerEndpointLimits, +} from '@canton-network/splice-pulumi-common/src/ratelimit/envoyRateLimiter'; + +// limits from https://cloud.google.com/armor/quotas#limits, in which a +// "subexpression" is an arg to && or || +export const MAX_SUBEXPRESSION_LENGTH = 1024; +export const MAX_EXPRESSION_LENGTH = 2048; + +// https://cloud.google.com/armor/quotas#limits +export const MAX_IPS_PER_RULE = 10; +// the default quota is 200 rules per security policy; leave headroom for the +// endpoint, WAF and default rules +export const MAX_IP_WHITELIST_RULES = 150; + +/** + * Splits the whitelisted source IP ranges into per-rule chunks, since a single Cloud + * Armor rule can only carry MAX_IPS_PER_RULE ranges. + * + * @param availablePriorities how many rule priority numbers are reserved for these rules + */ +export function ipWhitelistRuleChunks(ipRanges: string[], availablePriorities: number): string[][] { + const unique = [...new Set(ipRanges)].sort(); + if (unique.length === 0) { + throw new Error( + 'No whitelisted IP ranges for Cloud Armor: every internal endpoint would be denied' + ); + } + + const chunks = _.chunk(unique, MAX_IPS_PER_RULE); + if (chunks.length > MAX_IP_WHITELIST_RULES) { + throw new Error( + `${unique.length} whitelisted IP ranges need ${chunks.length} Cloud Armor rules, ` + + `which exceeds the ${MAX_IP_WHITELIST_RULES} rule budget (max ${MAX_IPS_PER_RULE} ranges per rule). ` + + `Consider using a network security address group instead.` + ); + } + if (chunks.length > availablePriorities) { + throw new Error( + `IP whitelist rules (${chunks.length}) would overlap the throttle rule priority range` + ); + } + return chunks; +} + +export function checkSubexpressionLength(context: string, expr: string): string { + if (expr.length > MAX_SUBEXPRESSION_LENGTH) { + throw new Error( + `Cloud Armor subexpression for ${context} exceeds the ${MAX_SUBEXPRESSION_LENGTH} character limit (current: ${expr.length}): ${expr}` + ); + } + return expr; +} + +/** + * Builds the host match condition for an endpoint, or undefined to match any host. + * + * A cluster is served under more than one DNS name (see getDnsNames), so a prefix match + * has to cover all of them, otherwise traffic on the other name falls through to the + * default deny rule. + * + * @param context config key of the endpoint, only used for error messages + * @param dnsNames all DNS names the cluster is served under + * @param hostname exact hostname to match + * @param hostPrefixRegex RE2 fragment matching the leading label(s) of a per-node hostname + */ +export function hostCondition( + context: string, + dnsNames: string[], + hostname?: string, + hostPrefixRegex?: string +): string | undefined { + let hostnameRegex; + if (hostname) { + hostnameRegex = _.escapeRegExp(hostname.toLowerCase()); + } else if (hostPrefixRegex) { + if (dnsNames.length === 0) { + throw new Error(`No cluster DNS names to build a host condition for ${context}`); + } + const dnsAlternatives = dnsNames.map(n => _.escapeRegExp(n.toLowerCase())).join('|'); + hostnameRegex = `(?:${hostPrefixRegex})\\.[\\w-]+\\.(?:${dnsAlternatives})`; + } else { + return undefined; + } + // the host header may carry a port, and Cloud Armor does not strip it + return checkSubexpressionLength( + context, + `request.headers['host'].lower().matches(R"^${hostnameRegex}(?::[0-9]+)?$")` + ); +} + +/** + * Builds the path match condition for an endpoint. + * + * When `restrictToRateLimitedPaths` is set, the condition is narrowed to exactly the + * paths under `pathPrefix` that the envoy rate limit config knows about. That is only + * correct for endpoints actually covered by those rate limits (scan and the token + * registry); for anything else it must be false, otherwise the rule would match none of + * the endpoint's real paths and the traffic would be denied by the default rule. + * + * @param context config key of the endpoint, only used for error messages + */ +export function allowedPathsCondition( + context: string, + scanExternalRateLimits: PerEndpointLimits, + pathPrefix: string, + restrictToRateLimitedPaths: boolean +): string { + // normalize before matching, see + // https://cloud.google.com/armor/docs/configure-security-policies#path-traversal-and-normalization + const simplePrefixMatch = () => + checkSubexpressionLength( + context, + `request.path.lower().urlDecode().startsWith(R"${pathPrefix.toLowerCase()}")` + ); + + if (!restrictToRateLimitedPaths || _.isEmpty(scanExternalRateLimits.rateLimits)) { + return simplePrefixMatch(); + } + + const basePrefix = pathPrefix.endsWith('/') ? pathPrefix : `${pathPrefix}/`; + const dynamicPathRxs = extractPathPrefixes(scanExternalRateLimits.rateLimits) + .filter(p => p.startsWith(basePrefix)) + .map(p => _.escapeRegExp(p.substring(basePrefix.length).toLowerCase())); + + if (dynamicPathRxs.length === 0) { + // no rate limited path lives under this prefix, so there is nothing to narrow to + return simplePrefixMatch(); + } + + const regexPattern = `${_.escapeRegExp(basePrefix.toLowerCase())}(?:${dynamicPathRxs.join('|')})`; + const pathExpr = `request.path.lower().urlDecode().matches(R"^${regexPattern}")`; + if (pathExpr.length > MAX_SUBEXPRESSION_LENGTH) { + throw new Error( + `Cloud Armor path expression for ${context} exceeds the ${MAX_SUBEXPRESSION_LENGTH} character limit (current: ${pathExpr.length}). ` + + `Consider grouping path prefixes more aggressively.` + ); + } + return pathExpr; +} + +/** + * Combines the path and host conditions into the full match expression of a rule. + */ +export function matchExpression( + context: string, + pathExpr: string, + hostExpr: string | undefined +): string { + const expr = [pathExpr, ...(hostExpr ? [hostExpr] : [])].join(' && '); + if (expr.length > MAX_EXPRESSION_LENGTH) { + throw new Error( + `Cloud Armor expression for ${context} exceeds the ${MAX_EXPRESSION_LENGTH} character limit (current: ${expr.length})` + ); + } + return expr; +} diff --git a/cluster/pulumi/infra/src/config.ts b/cluster/pulumi/infra/src/config.ts index a82d449669f..a6498404575 100644 --- a/cluster/pulumi/infra/src/config.ts +++ b/cluster/pulumi/infra/src/config.ts @@ -13,30 +13,92 @@ export const clusterBaseDomain = clusterHostname.split('.')[0]; export const gcpDnsProject = config.requireEnv('GCP_DNS_PROJECT'); +// https://cloud.google.com/armor/docs/rate-limiting-overview: intervalSec only +// accepts this fixed set of values, anything else is rejected by the GCP API. +const cloudArmorIntervalSeconds = [ + 10, 30, 60, 120, 180, 240, 300, 600, 900, 1200, 1800, 2700, 3600, +]; +// https://cloud.google.com/armor/docs/rate-limiting-overview: threshold count max +const cloudArmorMaxRateLimitCount = 1000000; + +const CloudArmorLoggingConfigSchema = z.object({ + enabled: z.boolean().default(false), + sampleRate: z.number().min(0).max(1).default(1), +}); + +export type CloudArmorLoggingConfig = z.infer; + const CloudArmorConfigSchema = z.object({ enabled: z.boolean(), // "preview" is not pulumi preview, but https://cloud.google.com/armor/docs/security-policy-overview#preview_mode allRulesPreviewOnly: z.boolean(), + logging: CloudArmorLoggingConfigSchema.prefault({}), publicEndpoints: z .object({}) .catchall( - z.object({ - rulePreviewOnly: z.boolean().default(false), - hostname: z - .string() - .regex(/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/, 'valid DNS hostname') - .optional(), - pathPrefix: z.string().regex(/^\/[^"]*$/, 'HTTP request path starting with /'), - throttleAcrossAllEndpointsAllIps: z.object({ - withinIntervalSeconds: z.number().positive(), - maxRequestsBeforeHttp429: z - .number() - .min(0, '0 to disallow requests or positive to allow'), - }), - }) + z + .object({ + rulePreviewOnly: z.boolean().default(false), + // exact hostname to match; mutually exclusive with hostPrefixRegex + hostname: z + .string() + .regex(/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/, 'valid DNS hostname') + .optional(), + // expanded to `..` for every DNS name + // the cluster is served under. E.g. `scan` or `sequencer-[0-9]+`. + // Mutually exclusive with hostname; if neither is given, any host matches. + hostPrefixRegex: z.string().optional(), + pathPrefix: z.string().regex(/^\/[^"]*$/, 'HTTP request path starting with /'), + // when true, the rule only matches the subset of paths under pathPrefix that + // are known. Must be false + // for endpoints whose paths are not part of that config (e.g. the sequencer + // gRPC APIs). + restrictToRateLimitedPaths: z.boolean().default(true), + // Per source IP throttling across all endpoints under this rule. + // + // Cloud Armor applies at most one rate limit per request: rules are evaluated + // in priority order and the first match wins, and a request under the + // threshold of a throttle rule takes its conformAction (always `allow`), + // which ends policy evaluation. So a global (enforceOnKey: ALL) rule and a + // per IP rule cannot both apply to the same traffic. We rate limit per IP + // here, since that is what stops an abusive source at the edge without one + // client being able to exhaust a bucket shared with everyone else; the global + // cap lives in envoy instead (see sv.scan.externalRateLimits.globalLimits), + // which is closer to the backend it protects. + // + // When omitted the endpoint is allowed without any Cloud Armor rate limiting. + throttleAcrossAllEndpointsPerIp: z + .object({ + withinIntervalSeconds: z + .number() + .refine( + n => cloudArmorIntervalSeconds.includes(n), + `must be one of ${cloudArmorIntervalSeconds.join(', ')}` + ), + maxRequestsBeforeHttp429: z + .number() + .int() + .min(0, '0 to disallow requests or positive to allow') + .max(cloudArmorMaxRateLimitCount), + }) + .optional(), + }) + .refine( + e => !(e.hostname && e.hostPrefixRegex), + 'at most one of hostname and hostPrefixRegex may be set' + ) + .refine( + e => !(e.restrictToRateLimitedPaths && e.pathPrefix === '/'), + 'restrictToRateLimitedPaths must be false when pathPrefix is /' + ) ) .default({}), }); +export const flowControlConfigSchema = z.object({ + initialStreamWindowSize: z.int(), + initialConnectionWindowSize: z.int(), + ports: z.array(z.number().int().positive()), +}); export const InfraConfigSchema = z.object({ infra: z.object({ ipWhitelisting: z @@ -55,9 +117,11 @@ export const InfraConfigSchema = z.object({ enablePublicTokenRegistry: z.boolean().default(false), enableGeneralIpWhitelist: z.boolean().default(false), istiodValues: z.object({}).catchall(z.any()).default({}), - sequencerFlowControl: z.object({ - initialStreamWindowSize: z.int(), - initialConnectionWindowSize: z.int(), + flowControl: z.object({ + // public APIs like the sequencer + public: flowControlConfigSchema, + // internal APIs like the participant + internal: flowControlConfigSchema, }), }), enableSweetSecurity: z.boolean().default(false), diff --git a/cluster/pulumi/infra/src/gcpLoadBalancer.ts b/cluster/pulumi/infra/src/gcpLoadBalancer.ts index 8574053f124..8620a0e859b 100644 --- a/cluster/pulumi/infra/src/gcpLoadBalancer.ts +++ b/cluster/pulumi/infra/src/gcpLoadBalancer.ts @@ -6,6 +6,7 @@ import * as pulumi from '@pulumi/pulumi'; import { CLUSTER_BASENAME, ExactNamespace } from '@canton-network/splice-pulumi-common'; import { CloudArmorPolicy } from './cloudArmor'; +import { CloudArmorLoggingConfig } from './config'; /* Any cluster that uses this must first run @@ -47,9 +48,16 @@ interface L7GatewayConfig { istioResource: pulumi.Resource; // the Cloud Armor policy to attach securityPolicy?: CloudArmorPolicy; + // LB backend service request logging, controlled via the cloudArmor.logging config + backendLogging?: CloudArmorLoggingConfig; // if provided, an HTTPS listener will be created on port 443 that // terminates TLS using this secret tlsSecretName?: pulumi.Input; + // hostnames that must not be evaluated by Cloud Armor. They are routed to a + // second k8s Service in front of the same istio ingress pods, which the GKE + // controller turns into its own GCP backend service, and only that backend + // service is left without a security policy. + cloudArmorExemptHostnames?: string[]; } const httpListenerName = 'listen-http'; @@ -129,29 +137,35 @@ type BackendTargetRef = { namespace: pulumi.Input; }; -function backendTargetRef(config: L7GatewayConfig): BackendTargetRef { +function backendTargetRef( + config: L7GatewayConfig, + // must be the name of the Service set up by the gateway + // *that is the backend of the L7 ALB gateway for which this is configured*. + // For a classic istio gateway this is the same as the 'name' set on the gateway helm chart; + // for a k8s istio gateway this is -istio. + // Can be identified by the apiVersion of the Gateway k8s resource + serviceName: pulumi.Input = config.backendServiceName +): BackendTargetRef { return { group: '', kind: 'Service', - // must be the name of the Service set up by the gateway - // *that is the backend of the L7 ALB gateway for which this is configured*. - // For a classic istio gateway this is the same as the 'name' set on the gateway helm chart; - // for a k8s istio gateway this is -istio. - // Can be identified by the apiVersion of the Gateway k8s resource - name: config.backendServiceName, + name: serviceName, namespace: config.ingressNs.ns.metadata.name, }; } /** - * Creates a GCPBackendPolicy for Cloud Armor integration + * Creates a GCPBackendPolicy for backend request logging and, if a policy is given, + * Cloud Armor integration. Omitting the policy leaves the backend service without a + * security policy, so requests routed to it are not evaluated by Cloud Armor. */ -function attachCloudArmorToLBBackend( - policy: CloudArmorPolicy, +function attachBackendPolicy( + policy: CloudArmorPolicy | undefined, config: L7GatewayConfig, - gateway: k8s.apiextensions.CustomResource + gateway: k8s.apiextensions.CustomResource, + policyName: string, + serviceName: pulumi.Input = config.backendServiceName ): k8s.apiextensions.CustomResource { - const policyName = `${config.gatewayName}-cloud-armor-link`; return new k8s.apiextensions.CustomResource( policyName, { @@ -163,20 +177,35 @@ function attachCloudArmorToLBBackend( }, spec: { default: { + // backend service request logging must be enabled for Cloud Armor + // rule decisions to show up in Cloud Logging + ...(config.backendLogging?.enabled + ? { + logging: { + enabled: true, + // rate out of 1'000'000, converted from the 0.0-1.0 config fraction + sampleRate: Math.round(config.backendLogging.sampleRate * 1000000), + }, + } + : {}), // if global vs regional is mismatched you'll see // SetSecurityPolicy: Invalid value for field 'resource': '{ "securityPolicy": "https://www.googleapis.com/compute/beta/projects/da-cn-scratchnet/regions/us-c...'. The given security policy does not exist - securityPolicy: policy.name.apply(name => { - console.assert( - !name.includes('/'), - `${name} should be just the name, not a full resource path` - ); - return name; - }), + ...(policy + ? { + securityPolicy: policy.name.apply(name => { + console.assert( + !name.includes('/'), + `${name} should be just the name, not a full resource path` + ); + return name; + }), + } + : {}), }, - targetRef: backendTargetRef(config), + targetRef: backendTargetRef(config, serviceName), }, }, - { parent: gateway, dependsOn: [policy] } + { parent: gateway, dependsOn: policy ? [policy] : [] } ); } @@ -186,9 +215,10 @@ function attachCloudArmorToLBBackend( */ function createHealthCheckPolicy( config: L7GatewayConfig, - gateway: k8s.apiextensions.CustomResource + gateway: k8s.apiextensions.CustomResource, + policyName: string = `${config.gatewayName}-healthcheck`, + serviceName: pulumi.Input = config.backendServiceName ): k8s.apiextensions.CustomResource { - const policyName = `${config.gatewayName}-healthcheck`; return new k8s.apiextensions.CustomResource( policyName, { @@ -209,13 +239,102 @@ function createHealthCheckPolicy( }, }, }, - targetRef: backendTargetRef(config), + targetRef: backendTargetRef(config, serviceName), }, }, { parent: gateway } ); } +/** + * The parentRef for routes serving actual traffic: the https listener when TLS is + * terminated at the gateway, the gateway itself otherwise. + */ +function mainRouteParentRef(config: L7GatewayConfig) { + return { + name: config.gatewayName, + namespace: config.ingressNs.ns.metadata.name, + ...(config.tlsSecretName ? { sectionName: httpsListenerName } : {}), + }; +} + +/** + * Routes the Cloud Armor exempt hostnames to their own k8s Service in front of the same + * istio ingress pods. + * + * The GKE Gateway controller creates one GCP backend service per (Service, port) pair + * referenced by a route, and Cloud Armor is attached per backend service via + * GCPBackendPolicy. Adding a second Service with the same pod selector therefore gives + * these hostnames a backend service that has no security policy attached, without + * needing a separate IP, DNS records, certificate SANs or istio ingress deployment. + * + * Gateway API precedence puts routes with matching hostnames ahead of the hostname-less + * catch-all route, so these hosts are served by this route rather than the main one. + */ +function createCloudArmorExemptBackend( + config: L7GatewayConfig, + gateway: k8s.apiextensions.CustomResource, + hostnames: string[] +): void { + const name = `${config.gatewayName}-no-cloud-armor`; + const service = new k8s.core.v1.Service( + name, + { + metadata: { + name, + namespace: config.ingressNs.ns.metadata.name, + }, + spec: { + type: 'ClusterIP', + selector: { app: 'istio-ingress' }, + ports: [ + { + name: 'http2', + port: config.serviceTarget.port, + targetPort: config.serviceTarget.port, + // force HTTP/2 (h2c) between the L7 gateway and istio-ingress, as the + // exempt traffic is gRPC + appProtocol: 'kubernetes.io/h2c', + }, + ], + }, + }, + { parent: gateway, dependsOn: [config.istioResource] } + ); + + new k8s.apiextensions.CustomResource( + `${name}-route`, + { + apiVersion: 'gateway.networking.k8s.io/v1', + kind: 'HTTPRoute', + metadata: { + name: `${name}-route`, + namespace: config.ingressNs.ns.metadata.name, + }, + spec: { + parentRefs: [mainRouteParentRef(config)], + hostnames, + rules: [ + { + backendRefs: [ + { + name: service.metadata.name, + namespace: config.ingressNs.ns.metadata.name, + port: config.serviceTarget.port, + }, + ], + }, + ], + }, + }, + { parent: gateway, dependsOn: [service] } + ); + + createHealthCheckPolicy(config, gateway, `${name}-healthcheck`, service.metadata.name); + // deliberately no security policy: that is the whole point of this backend + attachBackendPolicy(undefined, config, gateway, `${name}-backend-policy`, service.metadata.name); +} + /** * Creates HTTPRoutes for the L7 Gateway */ @@ -229,8 +348,6 @@ function createHTTPRoute(config: L7GatewayConfig, gateway: k8s.apiextensions.Cus const routeOpts = { parent: gateway }; - let sectionExtension: { sectionName: typeof httpsListenerName } | Record = {}; - // if we terminate TLS, make an extra redirect route and limit the main route // to the https listener if (config.tlsSecretName) { @@ -274,8 +391,6 @@ function createHTTPRoute(config: L7GatewayConfig, gateway: k8s.apiextensions.Cus }, routeOpts ); - - sectionExtension = { sectionName: httpsListenerName }; } // Main route, limited to https listener if enabled @@ -289,12 +404,7 @@ function createHTTPRoute(config: L7GatewayConfig, gateway: k8s.apiextensions.Cus namespace: config.ingressNs.ns.metadata.name, }, spec: { - parentRefs: [ - { - ...parentRef, - ...sectionExtension, - }, - ], + parentRefs: [mainRouteParentRef(config)], rules: [ { // default match, prefix path `/` @@ -380,11 +490,20 @@ export function configureGKEL7Gateway(config: L7GatewayConfig): { const gateway = createL7Gateway(config); if (config.securityPolicy) { - attachCloudArmorToLBBackend(config.securityPolicy, config, gateway); + attachBackendPolicy( + config.securityPolicy, + config, + gateway, + `${config.gatewayName}-cloud-armor-link` + ); } createHealthCheckPolicy(config, gateway); + if (config.cloudArmorExemptHostnames?.length) { + createCloudArmorExemptBackend(config, gateway, config.cloudArmorExemptHostnames); + } + const sslPolicy = createSSLPolicy(config); attachTLSPolicyToGateway(config, gateway, sslPolicy); diff --git a/cluster/pulumi/infra/src/index.ts b/cluster/pulumi/infra/src/index.ts index 5230bd1dd81..ec73850d038 100644 --- a/cluster/pulumi/infra/src/index.ts +++ b/cluster/pulumi/infra/src/index.ts @@ -21,6 +21,7 @@ import { configureIstio, istioVersion } from './istio'; import { deployGCPodReaper } from './maintenance'; import { configureNetwork } from './network'; import { configureReloader } from './reloader'; +import { sequencerP2pHosts } from './sequencerP2pHosts'; import { configureStorage } from './storage'; const network = configureNetwork(clusterBasename, clusterBaseDomain); @@ -57,6 +58,11 @@ if (useGKEL7Gateway) { serviceTarget: { port: 80 }, tlsSecretName: `cn-${clusterBasename}net-tls`, securityPolicy: cloudArmorSecurityPolicy, + backendLogging: cloudArmorConfig.logging, + // The sequencer BFT P2P API is mutually authenticated gRPC between known peers, so + // Cloud Armor adds no protection there while billing every P2P request. Route it to + // a backend service without a security policy attached. + cloudArmorExemptHostnames: sequencerP2pHosts(), istioResource: istio.istioResource, }); } diff --git a/cluster/pulumi/infra/src/istio.ts b/cluster/pulumi/infra/src/istio.ts index 5f46cdb2edf..0fdaba1f12f 100644 --- a/cluster/pulumi/infra/src/istio.ts +++ b/cluster/pulumi/infra/src/istio.ts @@ -10,6 +10,7 @@ import { import { cometBFTExternalPort } from '@canton-network/splice-pulumi-common-sv/src/synchronizer/cometbftConfig'; import { rateLimitResponseHeaders } from '@canton-network/splice-pulumi-common/src/ratelimit/rateLimitHeaders'; import { mergeWith } from 'lodash'; +import { z } from 'zod'; import { CLUSTER_HOSTNAME, @@ -24,7 +25,7 @@ import { isDevNet, isMainNet, } from '../../common'; -import { clusterBasename, infraConfig } from './config'; +import { clusterBasename, flowControlConfigSchema, infraConfig } from './config'; import { configureIstioGatewayPolicies, installAppWhitelisting } from './whitelisting'; import { loadInternalWhitelistedIps, loadIPRanges } from './whitelisting/ipRanges'; import { configurePublicInfo } from './whitelisting/publicInfo'; @@ -121,6 +122,10 @@ function configureIstiod( requested_server_name: '%REQUESTED_SERVER_NAME%', response_code: '%RESPONSE_CODE%', response_code_details: '%RESPONSE_CODE_DETAILS%', + // gRPC calls always end with HTTP 200, the outcome is in the gRPC status: a rate limited + // call is reported as `ResourceExhausted` (see `rate_limited_as_resource_exhausted`), + // whereas a rate limited HTTP request is reported as response_code 429 + grpc_status: '%GRPC_STATUS(CAMEL_STRING)%', response_flags: '%RESPONSE_FLAGS%', start_time: '%START_TIME%', upstream_cluster: '%UPSTREAM_CLUSTER%', @@ -129,7 +134,18 @@ function configureIstiod( upstream_service_time: '%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%', user_agent: '%REQ(USER-AGENT)%', x_forwarded_for: '%REQ(X-FORWARDED-FOR)%', + // the trusted client IP as determined by envoy (based on numTrustedProxies), + // this is the header the apps use for per-client-IP rate limiting + envoy_external_address: '%REQ(X-ENVOY-EXTERNAL-ADDRESS)%', + // The address the per-client-IP rate limit buckets are keyed on: envoy's + // `masked_remote_address` action masks the downstream remote address (the trusted, + // XFF-derived client address) with the configured prefix length, which is /32 for + // IPv4 and /128 for IPv6, i.e. the full address without the port. + masked_remote_address: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%', // rate limiting fields, will show up in sidecar access logging + // the value identifies the limit that rejected the request: `global`, `per_ip`, + // `endpoint` or `endpoint_per_ip`, i.e. the same names as the `limiter` label on the + // envoy_http_local_rate_limit_* metrics, which cannot attribute a single request to a limit local_rate_limited: '%RESP(x-local-rate-limit)%', rate_limit_limit: '%RESP(x-ratelimit-limit)%', rate_limit_remaining: '%RESP(x-ratelimit-remaining)%', @@ -719,10 +735,6 @@ function configureSequencerHighPerformanceGrpcDestinationRule( }); } -// Ports of the http2 servers that we apply the upstream flow control config to: -// the sequencer public API and the sequencer BFT P2P API. -const sequencerFlowControlUpstreamPorts = [5008, 5010]; - // Istio proxies lots of client connections over relatively few connections. If one of the client connections gets stuck // (e.g. because the client died) buffers will fill up and eventually istio will stop sending connection-level window updates // to the sequencer and trigger netty flow control. This surfaces as requests that send back response headers but then nothing else until the client times out. @@ -735,17 +747,16 @@ const sequencerFlowControlUpstreamPorts = [5008, 5010]; function configureSequencerFlowControl( ingressNs: k8s.core.v1.Namespace ): k8s.apiextensions.CustomResource { - const http2ProtocolOptions = { - initial_stream_window_size: infraConfig.istio.sequencerFlowControl.initialStreamWindowSize, - initial_connection_window_size: - infraConfig.istio.sequencerFlowControl.initialConnectionWindowSize, + const http2ProtocolOptions = (config: z.infer) => ({ + initial_stream_window_size: config.initialStreamWindowSize, + initial_connection_window_size: config.initialConnectionWindowSize, connection_keepalive: { interval: '30s', timeout: '5s', }, - }; - // istio -> upstream (aka sequencer) - const upstreamPatch = (portNumber: number) => ({ + }); + // istio sidecar of upstream -> upstream (e.g. sequencer) + const upstreamPatch = (portNumber: number, config: z.infer) => ({ applyTo: 'CLUSTER', match: { cluster: { @@ -762,7 +773,7 @@ function configureSequencerFlowControl( '@type': 'type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions', use_downstream_protocol_config: { http_protocol_options: {}, - http2_protocol_options: http2ProtocolOptions, + http2_protocol_options: http2ProtocolOptions(config), }, }, }, @@ -773,13 +784,13 @@ function configureSequencerFlowControl( apiVersion: 'networking.istio.io/v1alpha3', kind: 'EnvoyFilter', metadata: { - name: 'sequencer-flow-control', + name: 'flow-control', namespace: ingressNs.metadata.name, }, spec: { configPatches: [ { - // downstream (aka participant) -> istio + // downstream client (e.g. participant) -> istio sidecar of upstream (e.g. sequencer) applyTo: 'NETWORK_FILTER', match: { context: 'SIDECAR_INBOUND', @@ -797,12 +808,20 @@ function configureSequencerFlowControl( typed_config: { '@type': 'type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager', - http2_protocol_options: http2ProtocolOptions, + // This applies to both internal and public so we apply the more conservative internal limit + http2_protocol_options: http2ProtocolOptions( + infraConfig.istio.flowControl.internal + ), }, }, }, }, - ...sequencerFlowControlUpstreamPorts.map(upstreamPatch), + ...infraConfig.istio.flowControl.public.ports.map(p => + upstreamPatch(p, infraConfig.istio.flowControl.public) + ), + ...infraConfig.istio.flowControl.internal.ports.map(p => + upstreamPatch(p, infraConfig.istio.flowControl.internal) + ), ], }, }); diff --git a/cluster/pulumi/infra/src/sequencerP2pHosts.ts b/cluster/pulumi/infra/src/sequencerP2pHosts.ts new file mode 100644 index 00000000000..da102cb1bad --- /dev/null +++ b/cluster/pulumi/infra/src/sequencerP2pHosts.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + DecentralizedSynchronizerUpgradeConfig, + getDnsNames, +} from '@canton-network/splice-pulumi-common'; +import { allSvsToDeployBasic } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; + +/** + * The hostnames under which the sequencer BFT P2P API is served, one per + * (SV, BFT-enabled migration, cluster DNS name). + * + * These must stay in sync with the `p2pUrl`/`externalAddress` built in + * `common-sv/src/synchronizer/decentralizedSynchronizerNode.ts`. + */ +export function sequencerP2pHosts(): string[] { + const dnsNames = [getDnsNames().cantonDnsName, getDnsNames().daDnsName]; + return allSvsToDeployBasic.flatMap(sv => + DecentralizedSynchronizerUpgradeConfig.runningMigrations() + .filter(migration => migration.sequencer.enableBftSequencer) + .flatMap(migration => + dnsNames.map(dns => `sequencer-p2p-${migration.id}.${sv.ingressName}.${dns}`) + ) + ); +} diff --git a/cluster/pulumi/infra/src/whitelisting/sequencer.ts b/cluster/pulumi/infra/src/whitelisting/sequencer.ts index 5aa706c2d9d..2a0dabccbbd 100644 --- a/cluster/pulumi/infra/src/whitelisting/sequencer.ts +++ b/cluster/pulumi/infra/src/whitelisting/sequencer.ts @@ -8,6 +8,7 @@ import { } from '@canton-network/splice-pulumi-common'; import { allSvsToDeployBasic } from '@canton-network/splice-pulumi-common-sv/src/svConfigsBasic'; +import { sequencerP2pHosts } from '../sequencerP2pHosts'; import { loadIPRanges } from './ipRanges'; import { createIstioIpAllowPolicies, istioIngressSelector } from './policies'; @@ -25,16 +26,7 @@ export function configureSequencerWhitelist( ]) ) ); - const p2pHosts = allSvsToDeployBasic.flatMap(sv => - migrations - .filter(migration => migration.sequencer.enableBftSequencer) - .flatMap(migration => - dnsNames.flatMap(dns => [ - `sequencer-p2p-${migration.id}.${sv.ingressName}.${dns}`, - `sequencer-p2p-${migration.id}.${sv.ingressName}.${dns}:*`, - ]) - ) - ); + const p2pHosts = sequencerP2pHosts().flatMap(host => [host, `${host}:*`]); const policies = [ createIstioIpAllowPolicies({ diff --git a/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml b/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml index 774ba9e5026..66aeab5ed22 100644 --- a/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml +++ b/cluster/pulumi/observability/grafana-alerting/istio-rate-limiting_alerts.yaml @@ -19,11 +19,11 @@ groups: type: prometheus uid: prometheus editorMode: code - expr: sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~"sv.*"}[$__rate_interval])) by (namespace, pod) + expr: sum(rate(envoy_http_local_rate_limit_enforced{namespace=~"sv.*"}[$__rate_interval])) by (namespace, pod, limiter) instant: true interval: "" intervalMs: 30000 - legendFormat: '{{namespace}} - {{pod}}' + legendFormat: '{{namespace}} - {{pod}} - {{limiter}}' maxDataPoints: 43200 range: false refId: A @@ -59,9 +59,9 @@ groups: annotations: runbook_url: "" severity: warning - description: Envoy local rate limiting is active, requests are being rejected. Check the "Istio Rate Limiting" dashboard. - summary: Envoy is rate limiting requests in {{ $labels.namespace }} on pod {{ $labels.pod }}. + description: Envoy local rate limiting is active, requests are being rejected. The limiter label tells which limit rejected them (global, per_ip, endpoint_per_ip); the sidecar access log of a rejected request reports the same limit in the local_rate_limited field. Check the "Istio Rate Limiting" dashboard. + summary: Envoy is rate limiting requests in {{ $labels.namespace }} on pod {{ $labels.pod }} ({{ $labels.limiter }} limit). labels: "": "" - gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0AjsonPayload.response_code=429 + gcloud_filter: resource.labels.namespace_name=%22cluster-ingress%22%0A%28jsonPayload.response_code=429%20OR%20jsonPayload.grpc_status=%22ResourceExhausted%22%29 isPaused: false diff --git a/cluster/pulumi/observability/grafana-dashboards/platform/istio.json b/cluster/pulumi/observability/grafana-dashboards/platform/istio.json index eaee05bd499..d09046fa23f 100644 --- a/cluster/pulumi/observability/grafana-dashboards/platform/istio.json +++ b/cluster/pulumi/observability/grafana-dashboards/platform/istio.json @@ -25,7 +25,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Rate of HTTP 429 (Too Many Requests) responses per second, broken down by the namespace reporting the request and the destination service receiving it.", + "description": "Rate of rejected requests per second, broken down by the namespace reporting the request and the destination service receiving it. Rejections are HTTP 429 (Too Many Requests) responses, or, for gRPC calls (e.g. the sequencer API), RESOURCE_EXHAUSTED (grpc_response_status 8) responses, which envoy returns with HTTP 200.", "fieldConfig": { "defaults": { "color": { @@ -106,7 +106,7 @@ "targets": [ { "editorMode": "code", - "expr": "sum by(namespace, destination_service) (rate(istio_requests_total{response_code=\"429\", namespace=~\"$namespace\"}[$__rate_interval]))", + "expr": "sum by(namespace, destination_service) (rate(istio_requests_total{response_code=\"429\", namespace=~\"$namespace\"}[$__rate_interval])) + sum by(namespace, destination_service) (rate(istio_requests_total{grpc_response_status=\"8\", namespace=~\"$namespace\"}[$__rate_interval]))", "legendFormat": "__auto", "range": true, "refId": "A" @@ -120,7 +120,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Fraction of requests evaluated by the Envoy local rate limit filter that were rejected with HTTP 429, per namespace and pod.", + "description": "Fraction of the Envoy local rate limit evaluations that were rejected (HTTP 429, or gRPC RESOURCE_EXHAUSTED for gRPC APIs such as the sequencer), per namespace, pod and limiter. One filter per limit is installed (limiter=global, per_ip, endpoint_per_ip) and every request is evaluated by each of them.", "fieldConfig": { "defaults": { "color": { @@ -207,11 +207,11 @@ }, "editorMode": "code", "exemplar": false, - "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)\n/\nsum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", + "expr": "sum(rate(envoy_http_local_rate_limit_enforced{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod, limiter)\n/\nsum(rate(envoy_http_local_rate_limit_enabled{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod, limiter)", "format": "time_series", "instant": false, "interval": "", - "legendFormat": "{{namespace}} - {{pod}}", + "legendFormat": "{{namespace}} - {{pod}} - {{limiter}}", "range": true, "refId": "A" } @@ -224,7 +224,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Rate of number of requests for which rate limiting was applied (e.g.: 429 returned)", + "description": "Rate of number of requests for which rate limiting was applied (HTTP 429, or gRPC RESOURCE_EXHAUSTED for gRPC APIs such as the sequencer), broken down by the limit that rejected them: global, per_ip (per client IP) and endpoint_per_ip (per endpoint and client IP).", "fieldConfig": { "defaults": { "color": { @@ -306,8 +306,8 @@ "targets": [ { "editorMode": "code", - "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enforced{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", - "legendFormat": "{{namespace}} - {{pod}}", + "expr": "sum(rate(envoy_http_local_rate_limit_enforced{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod, limiter)", + "legendFormat": "{{namespace}} - {{pod}} - {{limiter}}", "range": true, "refId": "A" } @@ -402,8 +402,8 @@ "targets": [ { "editorMode": "code", - "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_rate_limited{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace,job)", - "legendFormat": "{{namespace}} - {{job}}", + "expr": "sum(rate(envoy_http_local_rate_limit_rate_limited{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, job, limiter)", + "legendFormat": "{{namespace}} - {{job}} - {{limiter}}", "range": true, "refId": "A" } @@ -498,8 +498,8 @@ "targets": [ { "editorMode": "code", - "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_enabled{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", - "legendFormat": "{{namespace}} - {{pod}}", + "expr": "sum(rate(envoy_http_local_rate_limit_enabled{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod, limiter)", + "legendFormat": "{{namespace}} - {{pod}} - {{limiter}}", "range": true, "refId": "A" } @@ -594,8 +594,8 @@ "targets": [ { "editorMode": "code", - "expr": "sum(rate(envoy_http_local_rate_limiter_http_local_rate_limit_ok{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod)", - "legendFormat": "{{namespace}} - {{pod}}", + "expr": "sum(rate(envoy_http_local_rate_limit_ok{namespace=~\"$namespace\"}[$__rate_interval])) by (namespace, pod, limiter)", + "legendFormat": "{{namespace}} - {{pod}} - {{limiter}}", "range": true, "refId": "A" } diff --git a/cluster/pulumi/observability/src/istio.ts b/cluster/pulumi/observability/src/istio.ts index eeee71f00d0..f33bc1602ae 100644 --- a/cluster/pulumi/observability/src/istio.ts +++ b/cluster/pulumi/observability/src/istio.ts @@ -3,6 +3,7 @@ import * as pulumi from '@pulumi/pulumi'; import { ExactNamespace } from '@canton-network/splice-pulumi-common'; import { PodMonitor, ServiceMonitor } from '@canton-network/splice-pulumi-common/src/metrics'; +import { rateLimiterMetricRelabelings } from '@canton-network/splice-pulumi-common/src/ratelimit'; export function istioMonitoring( ingressNs: ExactNamespace, @@ -42,6 +43,9 @@ export function istioMonitoring( regex: '(istio_.*' + '|envoy_.*http_local_rate_limit_.*)', action: 'keep', }, + // one local rate limit filter per limit is installed on the apps, each of them with + // its own stat prefix; merge them into a single metric labeled by limiter + ...rateLimiterMetricRelabelings, // drop instance label, we have the pod name { action: 'labeldrop', diff --git a/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts b/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts index 39b3fbf3489..f281cdcb085 100644 --- a/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts +++ b/cluster/pulumi/sv-canton/src/decentralizedSynchronizerNode.ts @@ -27,8 +27,10 @@ import { installCometBftNode, SingleSvConfiguration, StaticCometBftConfigWithNodeName, + svsConfig, } from '@canton-network/splice-pulumi-common-sv'; import { Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { installSequencerRateLimits } from '@canton-network/splice-pulumi-common/src/ratelimit/rateLimit'; import { Release } from '@pulumi/kubernetes/helm/v3'; import { ComponentResource, Output, Resource } from '@pulumi/pulumi'; @@ -193,6 +195,15 @@ abstract class InStackDecentralizedSynchronizerNode parent: this, } ); + + const sequencerRateLimits = svsConfig?.synchronizer?.sequencer?.externalRateLimits; + if (sequencerRateLimits) { + installSequencerRateLimits( + this.xns.logicalName, + `${this.name}-sequencer`, + sequencerRateLimits + ); + } } get namespaceInternalSequencerAddress(): string { diff --git a/cluster/pulumi/sv/src/installNode.ts b/cluster/pulumi/sv/src/installNode.ts index 5b6ba4ead33..eb06afdbb71 100644 --- a/cluster/pulumi/sv/src/installNode.ts +++ b/cluster/pulumi/sv/src/installNode.ts @@ -7,7 +7,6 @@ import { exactNamespace, imagePullSecretWithNonDefaultServiceAccount, installLedgerApiUserSecret, - spliceConfig, } from '@canton-network/splice-pulumi-common'; import { configForSv, @@ -15,38 +14,21 @@ import { svConfigs, svRunbookConfig, } from '@canton-network/splice-pulumi-common-sv'; -import { - installSvNodeStandalone, - MigrationArgs, - SvsMigrationOutput, -} from '@canton-network/splice-pulumi-common-sv/src/sv'; -import { StackReferences } from '@canton-network/splice-pulumi-common/src/stackReferences'; import { installParticipant } from './participant'; +import { installSvApps } from './sv'; export async function installNode(sv: string, auth0Client: Auth0Client): Promise { - // TODO(#6719) once all clusters have been migrated hardcode splitSvDeploymentEnabled to true - const splitSvDeploymentEnabled = - spliceConfig.configuration.synchronizerMigration.splitSvDeploymentEnabled; const staticConfig = findStaticConfigOrFail(sv); const config = configForSv(staticConfig.nodeName); - const xns = exactNamespace(staticConfig.nodeName, true, !splitSvDeploymentEnabled); + const xns = exactNamespace(staticConfig.nodeName, true, false); const serviceAccountName = 'sv'; const imagePullDeps = imagePullSecretWithNonDefaultServiceAccount(xns, serviceAccountName); const auth0Config = auth0Client.getCfg(); const ledgerApiUserSecret = installLedgerApiUserSecret(auth0Client, xns, 'sv', 'sv'); const ledgerApiUserSecretSource = auth0UserNameEnvVarSource('sv', true); - // TODO(#6719) once all clusters have been migrated remove this - const migrateToSplitSvDeployment = - spliceConfig.configuration.synchronizerMigration.migrateToSplitSvDeployment; - if ( - (splitSvDeploymentEnabled || migrateToSplitSvDeployment) && - staticConfig.nodeName !== svRunbookConfig.nodeName - ) { - const migrationArgs = migrateToSplitSvDeployment - ? await getMigrationArgsForSv(staticConfig.nodeName) - : undefined; - await installSvNodeStandalone(xns, staticConfig, config, auth0Client, [], migrationArgs); + if (staticConfig.nodeName !== svRunbookConfig.nodeName) { + await installSvApps(xns, staticConfig, config, auth0Client, []); } await installParticipant( { @@ -73,18 +55,3 @@ function findStaticConfigOrFail(sv: string): StaticSvConfig { return svConfig; } } - -// TODO(#6719) once all clusters have been migrated remove this -async function getMigrationArgsForSv(nodeName: string): Promise { - const svs = (await StackReferences.cantonNetwork.requireOutputValue('svs')) as SvsMigrationOutput; - const sv = - svs.find(sv => sv.nodeName === nodeName) ?? - (() => { - throw new Error(`No migration output found for SV: ${nodeName}`); - })(); - return { - action: 'import', - databaseInstanceName: sv.databaseInstanceName, - databaseSecretName: sv.databaseSecretName, - }; -} diff --git a/cluster/pulumi/sv/src/persistence.ts b/cluster/pulumi/sv/src/persistence.ts new file mode 100644 index 00000000000..49ff9e8e7f9 --- /dev/null +++ b/cluster/pulumi/sv/src/persistence.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as postgres from '@canton-network/splice-pulumi-common/src/postgres'; +import * as pulumi from '@pulumi/pulumi'; +import { ExactNamespace, PersistenceConfig } from '@canton-network/splice-pulumi-common'; +import { SvConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; + +export async function installAppsPostgres( + xns: ExactNamespace, + config: SvConfig +): Promise { + const defaultPostgres = config.splitPostgresInstances + ? undefined + : await postgres.installPostgres( + xns, + 'postgres', + 'postgres', + config.version, + spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, + false, + { + logicalDecoding: !!config.scanApp?.bigQuery, + } + ); + + const appsPostgres = + defaultPostgres || + (await postgres.installPostgres( + xns, + `cn-apps-pg`, + `cn-apps-pg`, + config.version, + config.appsPg?.cloudSql ?? spliceConfig.pulumiProjectConfig.cloudSql, + spliceConfig.pulumiProjectConfig.defaultSplicePostgresConfig, + true, + { + logicalDecoding: !!config.scanApp?.bigQuery, + } + )); + + return appsPostgres; +} + +export function persistenceConfig( + postgresDb: postgres.Postgres, + dbName: string +): PersistenceConfig { + const dbNameO = pulumi.Output.create(dbName); + return { + host: postgresDb.address, + databaseName: dbNameO, + secretName: postgresDb.secretName, + schema: dbNameO, + user: pulumi.Output.create('cnadmin'), + port: pulumi.Output.create(5432), + postgresName: postgresDb.instanceName, + }; +} diff --git a/cluster/pulumi/sv/src/scan.ts b/cluster/pulumi/sv/src/scan.ts new file mode 100644 index 00000000000..e196cb07bd0 --- /dev/null +++ b/cluster/pulumi/sv/src/scan.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as pulumi from '@pulumi/pulumi'; +import { + CLUSTER_HOSTNAME, + CnInput, + DecentralizedSynchronizerMigrationConfig, + ExactNamespace, + InstalledHelmChart, + envoyClientIpHeaderEnvVar, + failOnAppVersionMismatch, + getAdditionalJvmOptions, + installSpliceHelmChart, + persistentHeapDumpsPvc, + sanitizedForPostgres, + spliceInstanceNames, +} from '@canton-network/splice-pulumi-common'; +import { + CantonBftSynchronizerNode, + DecentralizedSynchronizerNode, + SynchronizerNodes, +} from '@canton-network/splice-pulumi-common-sv'; +import { SvConfig, svsConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; +import { Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { installRateLimits } from '@canton-network/splice-pulumi-common/src/ratelimit/rateLimit'; +import { Resource } from '@pulumi/pulumi'; + +import { persistenceConfig } from './persistence'; + +export function installScan( + xns: ExactNamespace, + config: SvConfig, + decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig, + dependsOn: CnInput[], + synchronizerNodes: SynchronizerNodes, + svApp: pulumi.Resource, + postgres: Postgres +): InstalledHelmChart { + const useCantonBft = decentralizedSynchronizerMigrationConfig.active.sequencer.enableBftSequencer; + const { active, participant } = synchronizerNodes; + const scanDbName = `scan_${sanitizedForPostgres(config.nodeName)}`; + + const cantonBftConfigFor = (node: DecentralizedSynchronizerNode) => { + return { + cantonBft: { + p2pUrl: (node as unknown as CantonBftSynchronizerNode).externalSequencerP2pAddress, + }, + }; + }; + + const synchronizerValues = { + synchronizers: { + current: { + sequencer: active.namespaceInternalSequencerAddress, + mediator: active.namespaceInternalMediatorAddress, + ...(useCantonBft ? cantonBftConfigFor(active) : {}), + }, + ...(synchronizerNodes.upgrade + ? { + successor: { + sequencer: synchronizerNodes.upgrade.namespaceInternalSequencerAddress, + mediator: synchronizerNodes.upgrade.namespaceInternalMediatorAddress, + ...(decentralizedSynchronizerMigrationConfig.upgrade?.sequencer.enableBftSequencer + ? cantonBftConfigFor(synchronizerNodes.upgrade) + : {}), + }, + } + : {}), + ...(synchronizerNodes.legacy + ? { + legacy: { + sequencer: synchronizerNodes.legacy.namespaceInternalSequencerAddress, + mediator: synchronizerNodes.legacy.namespaceInternalMediatorAddress, + ...(decentralizedSynchronizerMigrationConfig.legacy?.sequencer.enableBftSequencer + ? cantonBftConfigFor(synchronizerNodes.legacy) + : {}), + }, + } + : {}), + }, + }; + + const scanValues = { + ...spliceInstanceNames, + metrics: { + enable: true, + }, + isFirstSv: config.isFirstSv, + persistence: persistenceConfig(postgres, scanDbName), + additionalJvmOptions: getAdditionalJvmOptions(config.scanApp?.additionalJvmOptions), + failOnAppVersionMismatch: failOnAppVersionMismatch, + participantAddress: participant.internalClusterAddress, + ...(config.onboarding.type == 'join-with-key' + ? { sponsorScanUrl: config.onboarding.sponsorScanUrl } + : {}), + ...synchronizerValues, + enablePostgresMetrics: true, + logLevel: config.logging?.appsLogLevel, + apiRequestLogLevel: config.logging?.apiRequestLogLevel, + logAsyncFlush: config.logging?.appsAsync, + additionalEnvVars: (config.scanApp?.additionalEnvVars || []).concat([ + envoyClientIpHeaderEnvVar('canton.scan-apps.scan-app'), + ]), + resources: config.scanApp?.resources, + ...(config.bulkStorageBuckets + ? { + bulkStorage: { + staging: { + region: config.bulkStorageBuckets.staging.region, + bucketName: config.bulkStorageBuckets.staging.bucket.name, + endpoint: 'https://storage.googleapis.com', // gcs endpoint for s3 + secretName: config.bulkStorageBuckets.staging.secret.metadata.name, + }, + committed: { + region: config.bulkStorageBuckets.committed.region, + bucketName: config.bulkStorageBuckets.committed.bucket.name, + endpoint: 'https://storage.googleapis.com', // gcs endpoint for s3 + secretName: config.bulkStorageBuckets.committed.secret.metadata.name, + }, + }, + } + : {}), + publicUrl: publicScanUrl(config), + pvc: persistentHeapDumpsPvc(), + }; + + if (svsConfig?.scan?.externalRateLimits) { + installRateLimits(xns.logicalName, 'scan-app', 5012, svsConfig.scan.externalRateLimits); + } + + return installSpliceHelmChart(xns, 'scan', 'splice-scan', scanValues, config.version, { + // TODO(#893) if possible, don't require parallel start of sv app and scan when using CantonBft + dependsOn: dependsOn + .concat(active.dependencies) + .concat( + spliceConfig.pulumiProjectConfig.interAppsDependencies && !useCantonBft + ? [svApp] + : [postgres] + ), + }); +} + +export function publicScanUrl(config: SvConfig): string { + return `https://scan.${config.ingressName}.${CLUSTER_HOSTNAME}`; +} + +export function internalScanUrl(config: SvConfig): string { + return `http://scan-app.${config.nodeName}:5012`; +} diff --git a/cluster/pulumi/sv/src/sv.ts b/cluster/pulumi/sv/src/sv.ts new file mode 100644 index 00000000000..9cdece73912 --- /dev/null +++ b/cluster/pulumi/sv/src/sv.ts @@ -0,0 +1,391 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as postgres from '@canton-network/splice-pulumi-common/src/postgres'; +import * as k8s from '@pulumi/kubernetes'; +import * as pulumi from '@pulumi/pulumi'; +import { + activeVersion, + ansDomainPrefix, + Auth0Client, + btoa, + CLUSTER_BASENAME, + CLUSTER_HOSTNAME, + CnInput, + config as envConfig, + DecentralizedSynchronizerUpgradeConfig, + ExactNamespace, + fetchAndInstallParticipantBootstrapDump, + imagePullSecret, + InstalledHelmChart, + installSpliceHelmChart, + installSvAppSecrets, + installValidatorOnboardingSecret, + isDevNet, + svCometBftGovernanceKeyFromSecret, + svCometBftGovernanceKeySecret, + SvIdKey, + svKeyFromSecret, + svOnboardingPollingInterval, + svValidatorTopupConfig, +} from '@canton-network/splice-pulumi-common'; +import { + approvedSvIdentities, + coreSvsToDeploy, + initialRound, + installScanBulkStorage, + installSvLoopback, + SingleSvConfiguration, + StaticSvConfig, + SynchronizerNodes, +} from '@canton-network/splice-pulumi-common-sv'; +import { SvConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { installInfo } from '@canton-network/splice-pulumi-common-sv/src/info'; +import { readBackupConfig } from '@canton-network/splice-pulumi-common-validator/src/backup'; +import { + mustInstallSplitwell, + mustInstallValidator1, + splitwellOnboarding, + standaloneValidatorOnboarding, + validator1Onboarding, +} from '@canton-network/splice-pulumi-common-validator/src/validators'; +import { installBucketSecret } from '@canton-network/splice-pulumi-common/src/buckets'; +import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; +import { SplitPostgresInstances } from '@canton-network/splice-pulumi-common/src/config/configs'; +import { topologySnapshotConfig } from '@canton-network/splice-pulumi-common/src/topology-snapshot'; +import { Resource } from '@pulumi/pulumi'; +import pick from 'lodash/pick'; + +import { installAppsPostgres } from './persistence'; +import { installScan } from './scan'; +import { installSvApp } from './svApp'; +import { installValidator } from './validator'; + +export async function installSvApps( + xns: ExactNamespace, + staticConfig: StaticSvConfig, + dynamicConfig: SingleSvConfiguration, + auth0Client: Auth0Client, + extraDependsOn: CnInput[] = [] +): Promise { + const nodeName = staticConfig.nodeName; + const [sv1StaticConfig, ...otherSvsStaticConfigs] = coreSvsToDeploy; + const isFoundingSv = nodeName === sv1StaticConfig.nodeName; + const disableOnboardingParticipantPromotionDelay = envConfig.envFlag( + 'DISABLE_ONBOARDING_PARTICIPANT_PROMOTION_DELAY', + false + ); + const backupConfig = await readBackupConfig(); + const config: SvConfig = { + isFirstSv: isFoundingSv, + ...pick(staticConfig, [ + 'nodeName', + 'ingressName', + 'onboardingName', + 'cometBft', + 'validatorWalletUser', + 'auth0ValidatorAppName', + 'auth0SvAppName', + 'sweep', + ]), + nodeConfigs: { + sv1: { + ...sv1StaticConfig.cometBft, + ...pick(sv1StaticConfig, ['nodeName', 'ingressName']), + }, + peers: otherSvsStaticConfigs + .filter(config => config.nodeName !== nodeName) + .map(config => ({ + ...config.cometBft, + ...pick(config, ['nodeName', 'ingressName']), + })), + }, + onboarding: isFoundingSv + ? { + type: 'found-dso', + sv1SvRewardWeightBps: + approvedSvIdentities().find(identity => identity.name == sv1StaticConfig.onboardingName) + ?.rewardWeightBps ?? 10_000, + roundZeroDuration: envConfig.optionalEnv('ROUND_ZERO_DURATION'), + initialRound: initialRound?.toString(), + } + : { + type: 'join-with-key', + sponsorApiUrl: `http://sv-app.sv-1:5014`, + sponsorScanUrl: `http://scan-app.sv-1:5012`, + keys: svKeyFromSecret( + staticConfig.svIdKeySecretName ?? `${nodeName.replaceAll('-', '')}-id` + ), + }, + auth0Client, + expectedValidatorOnboardings: isFoundingSv + ? [ + ...(function* () { + if (mustInstallSplitwell) { + yield splitwellOnboarding; + } + if (mustInstallValidator1) { + yield validator1Onboarding; + } + if (standaloneValidatorOnboarding !== undefined) { + yield standaloneValidatorOnboarding; + } + })(), + ] + : [], + isDevNet, + periodicBackupConfig: backupConfig.periodicBackupConfig + ? { + ...backupConfig.periodicBackupConfig, + location: { + ...backupConfig.periodicBackupConfig.location, + prefix: + backupConfig.periodicBackupConfig.location.prefix || + `${CLUSTER_BASENAME}/${xns.logicalName}`, + }, + } + : undefined, + identitiesBackupLocation: { + ...backupConfig.identitiesBackupLocation, + prefix: `${CLUSTER_BASENAME}/${xns.logicalName}`, + }, + bootstrappingDumpConfig: backupConfig.bootstrappingDumpConfig, + bulkStorageBuckets: dynamicConfig.scanApp?.bulkStorage + ? installScanBulkStorage(xns, dynamicConfig.scanApp.bulkStorage) + : undefined, + topupConfig: svValidatorTopupConfig, + splitPostgresInstances: SplitPostgresInstances, + disableOnboardingParticipantPromotionDelay, + onboardingPollingInterval: svOnboardingPollingInterval, + cometBftGovernanceKey: + dynamicConfig.participant?.kms !== undefined + ? svCometBftGovernanceKeyFromSecret( + staticConfig.cometBftGovernanceKeySecretName ?? + `${nodeName.replaceAll('-', '')}-cometbft-governance-key` + ) + : undefined, + initialRound: initialRound?.toString(), + version: dynamicConfig.versionOverride ?? activeVersion, + ...dynamicConfig, + }; + + const periodicTopologySnapshotConfig = config.periodicSnapshots?.topology + ? await topologySnapshotConfig( + config.periodicSnapshots?.topology, + `${CLUSTER_BASENAME}/${xns.logicalName}` + ) + : undefined; + + const loopback = installSvLoopback(xns, DecentralizedSynchronizerUpgradeConfig.usesCometbft()); + const imagePullDeps = imagePullSecret(xns); + + const auth0Secrets: CnInput[] = await installSvAppSecrets( + xns, + config.auth0Client + ); + + const identitiesBackupConfigSecret = installBucketSecret( + xns, + config.identitiesBackupLocation.bucket + ); + + const topologySnapshotConfigSecret = periodicTopologySnapshotConfig + ? installBucketSecret(xns, periodicTopologySnapshotConfig.location.bucket) + : undefined; + const backupConfigSecret: pulumi.Resource | undefined = config.periodicBackupConfig + ? config.periodicBackupConfig.location.bucket != config.identitiesBackupLocation.bucket + ? installBucketSecret(xns, config.periodicBackupConfig.location.bucket) + : identitiesBackupConfigSecret + : undefined; + + const participantBootstrapDumpSecret: pulumi.Resource | undefined = config.bootstrappingDumpConfig + ? await fetchAndInstallParticipantBootstrapDump(xns, config.bootstrappingDumpConfig) + : undefined; + + const dependsOn: CnInput[] = auth0Secrets + .concat( + config.onboarding.type == 'join-with-key' + ? installSvKeySecret(xns, config.onboarding.keys) + : [] + ) + .concat( + config.onboarding.type == 'join-with-key' && + config.onboarding.sponsorRelease !== undefined && + spliceConfig.pulumiProjectConfig.interAppsDependencies + ? [config.onboarding.sponsorRelease] + : [] + ) + .concat( + config.expectedValidatorOnboardings.map(onboarding => + installValidatorOnboardingSecret(xns, onboarding.name, onboarding.secret) + ) + ) + .concat([identitiesBackupConfigSecret]) + .concat(backupConfigSecret ? [backupConfigSecret] : []) + .concat(topologySnapshotConfigSecret ? [topologySnapshotConfigSecret] : []) + .concat(participantBootstrapDumpSecret ? [participantBootstrapDumpSecret] : []) + .concat(loopback) + .concat(imagePullDeps) + .concat( + config.cometBftGovernanceKey + ? svCometBftGovernanceKeySecret(xns, config.cometBftGovernanceKey) + : [] + ) + .concat( + config.bulkStorageBuckets + ? [ + config.bulkStorageBuckets.staging.secret, + config.bulkStorageBuckets.staging.bucket, + config.bulkStorageBuckets.committed.secret, + config.bulkStorageBuckets.committed.bucket, + ] + : [] + ) + .concat(extraDependsOn); + + const appsPostgres = await installAppsPostgres(xns, config); + + const canton = new SynchronizerNodes( + DecentralizedSynchronizerUpgradeConfig, + { + ...config.nodeConfigs, + self: { ...config.cometBft, nodeName: config.nodeName }, + }, + config.ingressName + ); + + const svApp = installSvApp( + DecentralizedSynchronizerUpgradeConfig, + { ...config, periodicTopologySnapshotConfig }, + xns, + dependsOn, + appsPostgres, + canton + ); + + const scan = installScan( + xns, + config, + DecentralizedSynchronizerUpgradeConfig, + dependsOn, + canton, + svApp, + appsPostgres + ); + + installInfo( + xns, + `info.${config.ingressName}.${CLUSTER_HOSTNAME}`, + 'cluster-ingress/cn-http-gateway', + DecentralizedSynchronizerUpgradeConfig, + `http://scan-app.${config.nodeName}:5012`, + scan, + config.version + ); + + const validatorApp = await installValidator( + appsPostgres, + xns, + DecentralizedSynchronizerUpgradeConfig, + config, + backupConfigSecret, + canton, + svApp, + scan + ); + + const ingress = installSpliceHelmChart( + xns, + 'ingress-sv', + 'splice-cluster-ingress-runbook', + { + withSvIngress: true, + ingress: { + decentralizedSynchronizer: { + migrationIds: DecentralizedSynchronizerUpgradeConfig.runningMigrations().map(x => + x.id.toString() + ), + }, + }, + spliceDomainNames: { + nameServiceDomain: ansDomainPrefix, + }, + cluster: { + hostname: CLUSTER_HOSTNAME, + svNamespace: xns.logicalName, + svIngressName: config.ingressName, + }, + rateLimit: { + scan: { + enable: false, + }, + }, + }, + config.version, + { dependsOn: [xns.ns] } + ); + + return { + namespace: xns, + nodeName: config.nodeName, + canton, + validatorApp, + svApp, + scan, + ingress, + appsPostgres, + }; +} + +function installSvKeySecret(xns: ExactNamespace, keys: CnInput): k8s.core.v1.Secret[] { + const legacySecretName = 'cn-app-sv-key'; + const secretName = 'splice-app-sv-key'; + + const data = pulumi.output(keys).apply(ks => { + return { + public: btoa(ks.publicKey), + private: btoa(ks.privateKey), + }; + }); + + return [ + new k8s.core.v1.Secret( + `cn-app-${xns.logicalName}-key`, + { + metadata: { + name: legacySecretName, + namespace: xns.logicalName, + }, + type: 'Opaque', + data: data, + }, + { + dependsOn: [xns.ns], + } + ), + new k8s.core.v1.Secret( + `splice-app-${xns.logicalName}-key`, + { + metadata: { + name: secretName, + namespace: xns.logicalName, + }, + type: 'Opaque', + data: data, + }, + { + dependsOn: [xns.ns], + } + ), + ]; +} + +export type InstalledSv = { + namespace: ExactNamespace; + nodeName: string; + validatorApp: Resource; + svApp: InstalledHelmChart; + scan: InstalledHelmChart; + canton: SynchronizerNodes; + ingress: Resource; + appsPostgres: postgres.Postgres; +}; diff --git a/cluster/pulumi/sv/src/svApp.ts b/cluster/pulumi/sv/src/svApp.ts new file mode 100644 index 00000000000..c70a2238249 --- /dev/null +++ b/cluster/pulumi/sv/src/svApp.ts @@ -0,0 +1,157 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + appsKubernetesScheduling, + ChartValues, + CnInput, + daContactPoint, + DecentralizedSynchronizerMigrationConfig, + ExactNamespace, + failOnAppVersionMismatch, + getAdditionalJvmOptions, + getSvAppApiAudience, + initialPackageConfigJson, + initialSynchronizerFeesConfig, + InstalledHelmChart, + installSpliceHelmChart, + networkWideConfig, + participantBootstrapDumpSecretName, + persistentHeapDumpsPvc, + sanitizedForPostgres, + spliceInstanceNames, + validatorOnboardingSecretName, +} from '@canton-network/splice-pulumi-common'; +import { + approvedSvIdentities, + SynchronizerNodes, + valuesForSvApp, +} from '@canton-network/splice-pulumi-common-sv'; +import { SvConfig, svsConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { + delegatelessAutomationExpectedTaskDuration, + delegatelessAutomationExpiredRewardCouponBatchSize, + delegatelessAutomationExpiredRewardCouponNumBatches, +} from '@canton-network/splice-pulumi-common/src/automation'; +import { initialAmuletPrice } from '@canton-network/splice-pulumi-common/src/initialAmuletPrice'; +import { Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { Resource } from '@pulumi/pulumi'; + +import { persistenceConfig } from './persistence'; +import { publicScanUrl, internalScanUrl } from './scan'; + +export function installSvApp( + decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig, + config: SvConfig, + xns: ExactNamespace, + dependsOn: CnInput[], + postgres: Postgres, + synchronizerNodes: SynchronizerNodes +): InstalledHelmChart { + const { participant } = synchronizerNodes; + const decentralizedSynchronizer = synchronizerNodes.active; + const allSynchronizerDependencies = [ + synchronizerNodes.active, + synchronizerNodes.legacy, + synchronizerNodes.upgrade, + ...synchronizerNodes.additionalLegacy, + ] + .filter((n): n is NonNullable => n !== undefined) + .flatMap(n => n.dependencies); + const svDbName = `sv_${sanitizedForPostgres(config.nodeName)}`; + const commonSvAppValues = valuesForSvApp( + decentralizedSynchronizerMigrationConfig, + { ...config, skipInitialization: svsConfig?.synchronizer?.skipInitialization }, + synchronizerNodes, + config.ingressName + ); + + const svValues = { + ...commonSvAppValues, + ...spliceInstanceNames, + onboardingType: config.onboarding.type, + onboardingName: config.onboardingName, + onboardingFoundingSvRewardWeightBps: + config.onboarding.type == 'found-dso' ? config.onboarding.sv1SvRewardWeightBps : undefined, + onboardingRoundZeroDuration: + config.onboarding.type == 'found-dso' ? config.onboarding.roundZeroDuration : undefined, + initialSynchronizerFeesConfig: + config.onboarding.type == 'found-dso' ? initialSynchronizerFeesConfig : undefined, + initialPackageConfigJson: + config.onboarding.type == 'found-dso' ? initialPackageConfigJson : undefined, + initialRound: + config.onboarding.type == 'found-dso' ? config.onboarding.initialRound : undefined, + initialAmuletPrice: initialAmuletPrice, + disableOnboardingParticipantPromotionDelay: config.disableOnboardingParticipantPromotionDelay, + decentralizedSynchronizerUrl: + config.onboarding.type == 'found-dso' + ? undefined + : decentralizedSynchronizer.sv1InternalSequencerAddress, + scan: { + publicUrl: publicScanUrl(config), + internalUrl: internalScanUrl(config), + }, + expectedValidatorOnboardings: config.expectedValidatorOnboardings.map(onboarding => ({ + expiresIn: onboarding.expiresIn, + secretFrom: { + secretKeyRef: { + name: validatorOnboardingSecretName(onboarding.name), + key: 'secret', + optional: false, + }, + }, + })), + isDevNet: config.isDevNet, + approvedSvIdentities: approvedSvIdentities(), + persistence: persistenceConfig(postgres, svDbName), + identitiesExport: config.identitiesBackupLocation, + participantIdentitiesDumpImport: config.bootstrappingDumpConfig + ? { secretName: participantBootstrapDumpSecretName } + : undefined, + metrics: { + enable: true, + }, + additionalJvmOptions: getAdditionalJvmOptions(config.svApp?.additionalJvmOptions), + failOnAppVersionMismatch: failOnAppVersionMismatch, + participantAddress: participant.internalClusterAddress, + onboardingPollingInterval: config.onboardingPollingInterval, + enablePostgresMetrics: true, + auth: { + audience: getSvAppApiAudience(config.auth0Client.getCfg(), xns.logicalName), + jwksUrl: `https://${config.auth0Client.getCfg().auth0Domain}/.well-known/jwks.json`, + }, + contactPoint: daContactPoint, + nodeIdentifier: config.onboardingName, + delegatelessAutomationExpectedTaskDuration: delegatelessAutomationExpectedTaskDuration, + delegatelessAutomationExpiredRewardCouponBatchSize: + delegatelessAutomationExpiredRewardCouponBatchSize, + delegatelessAutomationExpiredRewardCouponNumBatches: + delegatelessAutomationExpiredRewardCouponNumBatches, + maxVettingDelay: networkWideConfig?.maxVettingDelay, + logLevel: config.logging?.appsLogLevel, + apiRequestLogLevel: config.logging?.apiRequestLogLevel, + logAsyncFlush: config.logging?.appsAsync, + resources: config.svApp?.resources, + periodicTopologySnapshotConfig: config.periodicTopologySnapshotConfig, + persistentDataPvc: persistentHeapDumpsPvc(), + } as ChartValues; + + if (config.onboarding.type == 'join-with-key') { + svValues.joinWithKeyOnboarding = { + sponsorApiUrl: config.onboarding.sponsorApiUrl, + sponsorScanUrl: config.onboarding.sponsorScanUrl, + }; + } + + return installSpliceHelmChart( + xns, + 'sv-app', + 'splice-sv-node', + svValues, + config.version, + { + dependsOn: dependsOn.concat([postgres]).concat(allSynchronizerDependencies), + }, + undefined, + appsKubernetesScheduling + ); +} diff --git a/cluster/pulumi/sv/src/validator.ts b/cluster/pulumi/sv/src/validator.ts new file mode 100644 index 00000000000..92a06f3d725 --- /dev/null +++ b/cluster/pulumi/sv/src/validator.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + DecentralizedSynchronizerMigrationConfig, + ExactNamespace, + sanitizedForPostgres, + svUserIds, +} from '@canton-network/splice-pulumi-common'; +import { + SynchronizerNodes, + valuesForSvValidatorApp, +} from '@canton-network/splice-pulumi-common-sv'; +import { SvConfig } from '@canton-network/splice-pulumi-common-sv/src/config'; +import { installValidatorApp } from '@canton-network/splice-pulumi-common-validator/src/validator'; +import { spliceConfig } from '@canton-network/splice-pulumi-common/src/config/config'; +import { Postgres } from '@canton-network/splice-pulumi-common/src/postgres'; +import { Resource } from '@pulumi/pulumi'; + +import { persistenceConfig } from './persistence'; +import { internalScanUrl } from './scan'; + +export async function installValidator( + postgres: Postgres, + xns: ExactNamespace, + decentralizedSynchronizerMigrationConfig: DecentralizedSynchronizerMigrationConfig, + svConfig: SvConfig, + backupConfigSecret: Resource | undefined, + sv: SynchronizerNodes, + svApp: Resource, + scan: Resource +): Promise { + const validatorDbName = `validator_${sanitizedForPostgres(svConfig.nodeName)}`; + const commonValidatorAppValues = valuesForSvValidatorApp( + decentralizedSynchronizerMigrationConfig, + svConfig + ); + + return await installValidatorApp({ + xns, + ...commonValidatorAppValues, + validatorWalletUsers: svUserIds(svConfig.auth0Client.getCfg()).apply(ids => + ids.concat(svConfig.validatorWalletUser ? [svConfig.validatorWalletUser] : []) + ), + dependencies: [], + disableAllocateLedgerApiUserParty: true, + topupConfig: svConfig.topupConfig, + backupConfig: + svConfig.periodicBackupConfig && backupConfigSecret + ? { + config: svConfig.periodicBackupConfig, + secret: backupConfigSecret, + } + : undefined, + persistenceConfig: persistenceConfig(postgres, validatorDbName), + extraDependsOn: spliceConfig.pulumiProjectConfig.interAppsDependencies + ? [svApp, postgres, scan] + : [postgres], + svValidator: true, + participantAddress: sv.participant.internalClusterAddress, + scanAddress: internalScanUrl(svConfig), + auth0Client: svConfig.auth0Client, + auth0ValidatorAppName: svConfig.auth0ValidatorAppName, + sweep: svConfig.sweep, + nodeIdentifier: svConfig.onboardingName, + logLevel: svConfig.logging?.appsLogLevel, + logAsync: svConfig.logging?.appsAsync, + apiRequestLogLevel: svConfig.logging?.apiRequestLogLevel, + additionalJvmOptions: svConfig.validatorApp?.additionalJvmOptions || '', + resources: svConfig.validatorApp?.resources, + version: svConfig.version, + }); +} diff --git a/cluster/scripts/node-restore.sh b/cluster/scripts/node-restore.sh index 8417504cacb..1bdfd4d51c6 100755 --- a/cluster/scripts/node-restore.sh +++ b/cluster/scripts/node-restore.sh @@ -100,6 +100,36 @@ function down() { done } +# Adapted from gc-pod-reaper. +# This is necessary because wait_down might block on pods that failed initializing +# e.g., (maybe) because GCP failed to schedule a pod due to Out of Resources +function kill_dead_pods() { + local -r namespace=$1 + local -r bad_pods=$( + kubectl get pods -n "$namespace" -o json | \ + jq -r '.items[] | + select( + (.status.phase == "Unknown" and .status.reason == "ContainerStatusUnknown") or + (.status.reason == "Evicted") or + (.status.containerStatuses[]?.state.terminated? | .reason == "Error" and .exitCode == 137) or + (.status.initContainerStatuses[]?.state.waiting?.reason == "ContainerStatusUnknown") + ) | .metadata.name' | sort -u + ); + if [ -z "$bad_pods" ]; then + echo "No bad pods found in $namespace. Skipping."; + return + fi + echo "Found bad pods in $namespace: $bad_pods"; + for pod_name in $bad_pods; do + echo "Attempting to delete pod $namespace/$pod_name"; + if kubectl delete pod -n "$namespace" "$pod_name" --ignore-not-found=true --timeout=180s; then + echo "Successfully deleted $namespace/$pod_name"; + else + echo "Failed to delete $namespace/$pod_name"; + fi + done +} + function wait_down() { local -r namespace=$1 local -r component=$2 @@ -469,6 +499,8 @@ function main() { down "$namespace" "$component" "$migration_id" done + kill_dead_pods "$namespace" + for component in "${components[@]}"; do wait_down "$namespace" "$component" "$migration_id" done diff --git a/cluster/scripts/utils.source b/cluster/scripts/utils.source index da6f79a103d..30b98ab8715 100755 --- a/cluster/scripts/utils.source +++ b/cluster/scripts/utils.source @@ -66,9 +66,6 @@ function get_stack_for_namespace_component() { local namespace=$1 local component=$2 - local split_sv_deployment - split_sv_deployment=$(get_resolved_config | yq '.synchronizerMigration.splitSvDeploymentEnabled // false') - local stack="" if [[ "${namespace}" =~ sv.* ]]; then if [[ "${component}" == "participant" ]]; then @@ -79,10 +76,8 @@ function get_stack_for_namespace_component() { stack="sv-canton" elif [[ "${component}" == "mediator" ]]; then stack="sv-canton" - elif [[ "${split_sv_deployment}" == "true" ]]; then - stack="sv" else - stack="canton-network" + stack="sv" fi else stack="$namespace" diff --git a/daml.yaml b/daml.yaml index 1636bd46bf4..0ab1bd41cca 100644 --- a/daml.yaml +++ b/daml.yaml @@ -1,5 +1,6 @@ sdk-version: 3.5.2 build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/dars.lock b/daml/dars.lock index a0d5d651ab8..458aee0bfe4 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -14,6 +14,7 @@ splice-amulet 0.1.2 1446ffdf23326cef2de97923df96618eb615792bea36cf1431f03639448f splice-amulet 0.1.20 23f47481dab6b1ec01339d6e14494d85bb2844c25f45b26fc5c9ef4cd4942d1f splice-amulet 0.1.21 73e9ffdb6b0bc19a5f67372b118103926da11547ab9109eccae47e4e4cc35d6f splice-amulet 0.1.22 fb10433a48c24f30076a7aee03a3e314b7ab02fe9a22e2069e3af92d2b6ac88a +splice-amulet 0.1.23 8fe7573f5535dc5b910a1f24d7d980da0e10ab38f8d29cb397146119bbfb7b3a splice-amulet 0.1.3 0d89016d5a90eb8bced48bbac99e81c57781b3a36094b8d48b8e4389851e19af splice-amulet 0.1.4 a36ef8888fb44caae13d96341ce1fabd84fc9e2e7b209bbc3caabb48b6be1668 splice-amulet 0.1.5 b4867a47abbfa2d15482f22c9c50516f0af14036287f299342f5d336391e4997 @@ -38,6 +39,7 @@ splice-amulet-name-service 0.1.20 0b6748de2e613f5bc9a108b2496b85d2d661b7344c6c55 splice-amulet-name-service 0.1.21 b342bbbd425902283c20eb5011eeab90cc9f69b4c534b8b757fd51b8ca7be589 splice-amulet-name-service 0.1.22 3921e7bd20670dce7530a84ad04b5f83860c21660bf1553908f76e95767c1950 splice-amulet-name-service 0.1.23 9cffe65feb664c9550937433067e9f969e3795c6fb38715e06a5e04fc1ae1f83 +splice-amulet-name-service 0.1.24 cc7d11e790174d2b18ad3e148d8762340e58de35616b399f9397a1b1d5b752f1 splice-amulet-name-service 0.1.3 beb4b85f3f0cf36dfb93fc917d3ac218ee5d41b6e70604720cb228d85e168ee0 splice-amulet-name-service 0.1.4 053c7f4c2a77312e7d465a4fa7dc8cb298754ad12c0c987a7c401bd724e65efc splice-amulet-name-service 0.1.5 6188c8b5f612278f988fc95c11e9742993ad3ac6ad0809f9af06ee9d366dc4a8 @@ -45,8 +47,8 @@ splice-amulet-name-service 0.1.6 a208aab2c4a248ab2eff352bd382f8b3bbadc92464123db splice-amulet-name-service 0.1.7 ba7806d9b2d593eac74a050161c54ae1325d170bf175cb66a9c1e5e5ffb88c3d splice-amulet-name-service 0.1.8 efeb3f9b2b92e55fac4ec2d6164f95407a01477240c7465e576df4e310f54bd3 splice-amulet-name-service 0.1.9 f1b5915ad45ded616f43f83c735b7ee158b5eb58abe758a721e50eee19b3e531 -splice-amulet-name-service-test 0.1.27 a1cda452d49452295766414515d04d4bc411eb39961db15cf41339bedec7bf63 -splice-amulet-test 0.1.26 4badc6beb226a7a0e4216f5d58489d4a41db0bd3b53a3349dd62025268f4cbd5 +splice-amulet-name-service-test 0.1.28 f985db437d124f01644be5801409d6e8cebfd60b66797f03f61f7aaa6a51010c +splice-amulet-test 0.1.27 009437315ddd604c8400a663c9d2c92c10823537bdab90f41d703b93cdb931bc splice-api-featured-app-v1 1.0.0 7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda splice-api-featured-app-v2 1.0.0 dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea splice-api-reward-assignment-v1 1.0.0 6f7b72361bc2039369651b4195315a2a5849babafec67b3c96e66ea6e560ec35 @@ -87,6 +89,7 @@ splice-dso-governance 0.1.25 dfe102514acd41f79945b8363ca84d8243be97b2b235522ce77 splice-dso-governance 0.1.26 45099e955ce443f7895125097a62c509b9ad297091c91d6377ffe94f183c0e3e splice-dso-governance 0.1.27 014473d35514cb36583bdeca269386e46ae4e2283fe9b04beb2f53cdb408e1a2 splice-dso-governance 0.1.28 eeb461e9e430d8aaaa232f23058a28c9858d559abd083c391d7ab7e73efb4e13 +splice-dso-governance 0.1.29 2b680f34f11be43d65a2dde723f4c9595a0983f63a0bb3455f03bb16c5d9b814 splice-dso-governance 0.1.3 b0ae3cc03e418790305a3c15f761fe495572de5827f8d322fb8b96996b783c13 splice-dso-governance 0.1.4 dc24fd18b4d151cd1e0ff6bfb7438bafb2f50fe076d0f16f50565e60b153a0be splice-dso-governance 0.1.5 9e3ca1d22ad495dfabf3d61acae3dc1a7718f527f02092280b58cf69edfdc84c @@ -94,23 +97,28 @@ splice-dso-governance 0.1.6 4e7653cfbf7ca249de4507aca9cd3b91060e5489042a522c589d splice-dso-governance 0.1.7 d406eba1132d464605f4dae3edf8cf5ecbbb34bd8edef0e047e7e526d328718c splice-dso-governance 0.1.8 1790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948 splice-dso-governance 0.1.9 9ee83bfd872f91e659b8a8439c5b4eaf240bcf6f19698f884d7d7993ab48c401 -splice-dso-governance-test 0.1.34 e94fbac6918872a6afb2535cbdd2029cba83365dc7edd81b331820ff6c5a8048 +splice-dso-governance-test 0.1.35 06ac1dab858f612a840f8258d8ee8d419099b1d5c3bf6cdf1e0fef6787a54a89 splice-test-token-v1 1.0.0 aaa0b576b5a3db49b3a4f7a4710fe6f8ae462aabbde4da89f639eb87fd62e90c -splice-test-token-v1-test 1.0.1 4eafb4e9b05999f4bcb28454fd4e691515a37ffe4b79cb118d5872aa1b82ab58 +splice-test-token-v1 1.0.1 da294b5651ed00af8565fd31351d0c1d8778f10393d250e32c9844311f681436 +splice-test-token-v1-test 1.0.2 881e08d322a8b2426f4def049b452e3061125f50c17e0219eba613d13a6bb397 splice-test-token-v2 1.0.0 a38a96b6f46c14c599b2763bc4fc68911a9cada90f89c599a1401e8e3df685e1 -splice-test-token-v2-test 1.0.1 b9cb700920834987855b037d2f03a9351c81039421f3c9c1e58624f3a7ee6604 +splice-test-token-v2 1.0.1 6f2bddc934eb700ee00afa209eb1d49f861af6401037618f6aa7e6ac02c8e201 +splice-test-token-v2-test 1.0.2 db4074ebfd9ca3b233925c4592a63cbfa7b2d47cceb2a6cab8a40d58ba2fd569 splice-token-standard-utils 2.0.0 9a8f41a2b1456d357dee5677565c21b9a5aa45b80f0ed6be469694445dd4f6e1 -splice-token-standard-utils-test 1.0.1 a781fc41338d98cfba49e3f2d4413d3cc4ecf6c2fcd23a5e7ba979abcbe02be4 -splice-token-standard-v1-test 1.0.16 2f112eaf6c6b6696fd7ba5cb932f79bf5e01177b19e28b3df9be5c6b16f75126 -splice-token-standard-v2-test 1.0.1 178b13ee29b21d3889971304eefa2f87d482c73779169f72efd722b8efe6d18d +splice-token-standard-utils-test 1.0.2 f8e88711605c176a14fa105dff3bbbc5168dddc5d5598a444b198fac1179ca14 +splice-token-standard-v1-test 1.0.17 47cc1484a349b2602fdc5c32224710a79b4748f29f6d6050777ee42134bca040 +splice-token-standard-v2-test 1.0.2 531dcbfe039a3e8259535e873c9ecd6fbe6fd6f3871c1d0bfd0fc32f85be53b8 splice-token-test-dummy-holding 0.0.1 1cd171c6c42ab46dc9cf12d80c6111369e00cea5cdf054924b4f26ce94b1ef5b splice-token-test-dummy-holding 0.0.2 4f40fb033ef3db89623642c1b494e846097fa32af138b3864a63aa15937a323d splice-token-test-dummy-holding 0.0.3 26c72abb5a4b485e58f201021de6f9e525c85863fa115536f768d6ee138ef13e splice-token-test-dummy-holding 0.0.4 1385c84f161e8d63593b4ac58149642fc2d15cea813861df3231bb47d8be2907 +splice-token-test-dummy-holding 0.0.5 a3efeac51e33818d1ffcef1ad549caabf5c8d5810709d03ba162bc8e321e74d9 splice-token-test-trading-app 1.0.0 e5c9847d5a88d3b8d65436f01765fc5ba142cc58529692e2dacdd865d9939f71 splice-token-test-trading-app 1.0.1 833ea19115848d050341dfd9f10cb7813ffa17b8d8dc40d702b2b6adbbe31697 splice-token-test-trading-app 1.0.2 88988edeabc5fcea383d5d0a7333c1f71468be4e9e1684a640f766eca32a0ed1 +splice-token-test-trading-app 1.0.3 76913ca0dc24309b720e6b0133363f40b30b78433d581d46f4a9cc8b61715eca splice-token-test-trading-app-v2 1.0.0 20d42271cbd760c26c08401e4d1eec624458f5b48c73ae44c87c754e35621f5d +splice-token-test-trading-app-v2 1.0.1 c01d9742db7c7c515dcd8cb2ff4b186c0957bfc39f6020db424b1cc80d08c8e4 splice-util 0.1.0 0f319b3bcc9a418836d58c86deae25b38d3f018f1b0c117456cd9011cface7f1 splice-util 0.1.1 00bf3632ca479d56e536096ca23bbc75d15084088ab5d12e4b31d6547d1df351 splice-util 0.1.2 3eb8f9ff160b782e0bf7ef0351072cfb6f186f086e082b35d9f7a0317e163372 @@ -119,11 +127,13 @@ splice-util 0.1.4 b7356fbb2cf8a3b22194d8c743c3c216d9c7527b257c8c38b257eb22942be3 splice-util 0.1.5 5a58024e2cc488ca9e0c952ec7ef41da3a1ed0a78ba23bacd819e5b30afb5546 splice-util 0.1.6 62a6fc80fc8ca84dd5f6b33c71f53dd35a02bc01784b8cbc7cbd283eb23863c0 splice-util 0.1.7 b5be6316177384677c024316c0bfaf7d3d494e7c3ad6c540d2032e6112b004bc +splice-util 0.1.8 411fec054a5cadb4291604facc75e1b51c1a13515e18664d696699f822e49124 splice-util-batched-markers 1.0.0 727c5e97457d3ff841680816eb70d55834827ef756bac8551cace5b961c9c1d2 splice-util-batched-markers 1.0.1 4d91a9b044e0e996e91ee9aac3442591ffc78f16da4ff5c6f55218ba667f6192 splice-util-batched-markers 1.0.2 ec84eb67802403d91892aad61aee2e4eb34fb4e05e9158b3f677537569bcbc09 splice-util-batched-markers 1.0.3 74ee4c91b80c80b6860005ce627bdaed8f7fe6bdf3e60c9dbf3f48402d55126a -splice-util-batched-markers-test 1.0.9 bbb2e18ad1b75f075f7edf70def9faf69f1cc18d707e65ab9b5f14283fc77aa5 +splice-util-batched-markers 1.0.4 c442a23fc47319108d6408ca0386b6087c255f79bf180a6aa77a807a985085c2 +splice-util-batched-markers-test 1.0.10 c51de5269c9faeb5be85451b2dce28b0244be28bdb932305a81ef7ac9f36254d splice-util-featured-app-proxies 1.0.0 48e0c4fe4ea05e3b740404ebe37004ddd741efbdcd665c1c3199a5d6d9d944d7 splice-util-featured-app-proxies 1.1.0 81dd5a9e5c02d0de03208522a895fb85eeb12fbea4aca7c4ad0ad106f3b0bfce splice-util-featured-app-proxies 1.2.0 653c48879064332d34af5008bdfd8e349493460e67e62b85e8e7e3392831c842 @@ -131,11 +141,12 @@ splice-util-featured-app-proxies 1.2.1 06bab917848ef275317c2539b75c23b94e03ceb55 splice-util-featured-app-proxies 1.2.2 2889c094cf9678b2b666221934ea56ab169a31b257450845bd53217a8cdfe44f splice-util-featured-app-proxies 1.2.3 677ed3473e7d412c06520843790ff27e21aaa7c292208909579592351c8eb55d splice-util-featured-app-proxies 1.2.4 88bcea6e9990bb2edb5301c042caa25c0594742665866f049f7bd67342d0865d -splice-util-featured-app-proxies-test 1.0.15 97e71bda26ffd878a4cac2a237b931ef59da3b3b60d82e1fe6cb248492fc290e +splice-util-featured-app-proxies-test 1.0.16 2b4ea32ea894b0f66f5c12655fe8fb3eeaa9345f49c7b7097e55627440017ba6 splice-util-token-standard-wallet 1.0.0 1da198cb7968fa478cfa12aba9fdf128a63a8af6ab284ea6be238cf92a3733ac splice-util-token-standard-wallet 1.0.1 182892381e4245c39d3126082ec0b41a089edb61e63af518f61eae1f5c9e135a splice-util-token-standard-wallet 1.1.0 1215d3ee8f3cb428d062d8708c2deca33c91ff99f5edefa70616646e57f93012 -splice-util-token-standard-wallet-test 1.0.10 873afaaee4cf3358d23c328e134474e19df11fffcc3fd1a4b837b64a7ed0bb6b +splice-util-token-standard-wallet 1.1.1 5acc49b7a6a41f6a3edbf9d7f9efae83cb2d97fb0ad67de52b2052922fa1d32d +splice-util-token-standard-wallet-test 1.0.11 dccf90ab650e87183667848700b14a7e0b87ef5ea75da20ed35f2de832b5b8c4 splice-validator-lifecycle 0.1.0 cef96fac957362f1fc097120bd13686cac7f84fbc8053afa994a1f9214d9570c splice-validator-lifecycle 0.1.1 1ddf05c96002914593c929848b786f34c753fb0be07717d1786be177a564aada splice-validator-lifecycle 0.1.2 57e2f15f9755db1f00e51c52c319294264a21ad71c6bc1e7cd70db4b164c0aaa @@ -145,7 +156,8 @@ splice-validator-lifecycle 0.1.5 455dd4533c2dd0131fb349c93d9d35f3670901d13efadb0 splice-validator-lifecycle 0.1.6 a717b2b9c23f630eb1ac907445fa5f5c9cce87fca28374b6165bc510724e07e7 splice-validator-lifecycle 0.1.7 9018d59e7759bfe37f242241b332930d32a57dc3e029525a441dde3d1e7720b0 splice-validator-lifecycle 0.1.8 217edf88c015ee080de2132a208cf23c14e9a2773198c0d4078985ab25ad4be1 -splice-validator-lifecycle-test 0.1.8 46caafa26c69153700a7175fb1a51f93e215e5bef5d656edc5d4307c51870d1f +splice-validator-lifecycle 0.1.9 639d2fd54431a2432bc9394917b1a6f664e79cfb898cd5487462b6ef8a282798 +splice-validator-lifecycle-test 0.1.9 d13d01f221152ec7697904febbe5367a7fa38eb5641f916d9f2a5aee84d187dc splice-wallet 0.1.0 5f384fba007e8b93c8ff1ac151cdc2d035ae1e0506f5e5770de469048713024c splice-wallet 0.1.1 e276113de450c2bb4bf8cf2d2991dd0b397fe7fe8103fe1b5377b8ab6622edec splice-wallet 0.1.10 9824ffa414d5803d17efafd75c69c1dd53591ac5197d07048b24dbac1720a462 @@ -163,6 +175,7 @@ splice-wallet 0.1.20 e485c2eb59cca556e0e2ecfaaef45346dd0ba64f9976a3957938d814f84 splice-wallet 0.1.21 f799a58fa53dfe48bae52bd5dbcc2b578a7d4dfee3ae3f4eb7635fe9a8cc67d3 splice-wallet 0.1.22 a78daeeb1a4a2405828b29f8c04814725dbd97ee5a60bd02e839f32518640afa splice-wallet 0.1.23 727c0fb9711d59b506fbb3cff2342e55eb583bb88682a946220525c304feca4a +splice-wallet 0.1.24 d42261314d98cc04bf50bcc58930af98b9f33aeed11339465925de697d3ba66e splice-wallet 0.1.3 2c35bb4f5084ea66db59717d21750bfd64c43147ef5fd5166615092d592a6917 splice-wallet 0.1.4 141dad2d33b6410b8e1c35a0c4f8f76cb691e4d9a4410ce89f33f373855317e1 splice-wallet 0.1.5 614b525a50c624062d851ce7df5bdb90ddfa0d6871c486cb6e2c7b694bfbce59 @@ -186,6 +199,7 @@ splice-wallet-payments 0.1.2 775f5eb9c0249509adda5eb3ea4ee31bb953601168c18880df6 splice-wallet-payments 0.1.20 340afccb702179069fc81044890c435c5c8565df061b2efbfccf618c33a04e37 splice-wallet-payments 0.1.21 dc1a2f4ff4774e00c388f1bfce48a23c3539b8ea0d0ebf99b8df840a058ef873 splice-wallet-payments 0.1.22 866ea53bea2afc099c560ad4c386a1736859841b17c422836f133d605ee51616 +splice-wallet-payments 0.1.23 d7297024d6a3dc7a81509556af0e686dd5a3ff21f8db1febcfbf49cf3dab195a splice-wallet-payments 0.1.3 b953b3729c81a55e598a364be7d0c0574750df3de12a7a1b53a300f217cb5c5c splice-wallet-payments 0.1.4 12177f54873c1094ea169874ad0d7838383fd137f302d16356e93f28dfbc0fcc splice-wallet-payments 0.1.5 2177b7a579fe6d3d510c7bcda99d39ece104ae24cfd1b78ac5616990d2d6c675 @@ -193,7 +207,7 @@ splice-wallet-payments 0.1.6 6124379528eeb6fa17ecdab15577c29abb33d0c0d34dc5f2680 splice-wallet-payments 0.1.7 4e3e0d9cdadf80f4bf8f3cd3660d5287c084c9a29f23c901aabce597d72fd467 splice-wallet-payments 0.1.8 e48ea337ee3335c8bb3206a2501ce947ac1a7bdb1825cee8f28bad64f5a7bc4b splice-wallet-payments 0.1.9 7f4e081ad96f2ccded0c053b0cf5ddddae1139dfc3bb89cefcf77ea70f2cecb7 -splice-wallet-test 0.1.27 b697558851c8f11e345945c67c1f8385f879bc3ece4ed840c8c386fd3e4f7b55 +splice-wallet-test 0.1.28 f5d3095e36834fb948f21f7d04e38c92eb61395ced670de8b09f85284c842815 splitwell 0.1.0 075c76de553ab88383a7c69de134afa82aacfdf8ea8fcfe8852c4b199c3b2669 splitwell 0.1.1 ccb1a0215053062202052e1a052f9214da3fdae5253a6d43e2e155ff4f57fe75 splitwell 0.1.10 d42676a366f7ca7a2409974dd3054aa4d83ab29baa3b2086ad021407b0a1a295 @@ -211,6 +225,7 @@ splitwell 0.1.20 af686139dda12540ab05a292987f5fa2edd0c05af62d9651384d005e9a91043 splitwell 0.1.21 f646cf275b4c5139711469c92fbe702c498614edf200d8085305b2e43e28936a splitwell 0.1.22 b97352bf1b70f6be481d7cd2b4ce22396008bbf611b488243a694f30437ed3a9 splitwell 0.1.23 68d3911f72347dc787c336f73af3c036004c982f591cb55e7b4f2fb45f5e50e6 +splitwell 0.1.24 f2ff9522cd65a8ed15c62c709a37e027fcd879ec5eab9e1476bde67b33deaec8 splitwell 0.1.3 7cde068cde689584f86a2499689d5cb165264d96496721e24ac6fb909f770a58 splitwell 0.1.4 85557b86cd4f330f093915db1ea26eac5092de6b5ddae0690146f6059c89419b splitwell 0.1.5 a68e78774a7be655f5744c8ae0ac8b46d55ef6d1e7661bc27b9296154d56ac74 @@ -218,4 +233,4 @@ splitwell 0.1.6 872da0dd7986fd768930f85d6a7310a94a0ef924e7fbb7bb7a4e149f2b5feb74 splitwell 0.1.7 841d1c9c86b5c8f3a39059459ecd8febedf7703e18f117300bb0ebf4423db096 splitwell 0.1.8 63b8153a08ceb4bf40d807acc5712372c3eac548c266be4d5e92470b4f655515 splitwell 0.1.9 b6267905698d2798b9ef171e27d49fb88e052ec0ec0e0675a3a1b275c7d037d4 -splitwell-test 0.1.27 b606a7cc393caae66e79ea10f40611d8629013c955118b54c0c85e11f2d87c6a \ No newline at end of file +splitwell-test 0.1.28 13748cd83d441727eee436b86776393020dc7f9fde33cbc1eaf7289b029428f1 \ No newline at end of file diff --git a/daml/dars/splice-amulet-0.1.23.dar b/daml/dars/splice-amulet-0.1.23.dar new file mode 100644 index 00000000000..75693eac6a0 Binary files /dev/null and b/daml/dars/splice-amulet-0.1.23.dar differ diff --git a/daml/dars/splice-amulet-name-service-0.1.24.dar b/daml/dars/splice-amulet-name-service-0.1.24.dar new file mode 100644 index 00000000000..8dc0497c538 Binary files /dev/null and b/daml/dars/splice-amulet-name-service-0.1.24.dar differ diff --git a/daml/dars/splice-dso-governance-0.1.29.dar b/daml/dars/splice-dso-governance-0.1.29.dar new file mode 100644 index 00000000000..b15b6086d69 Binary files /dev/null and b/daml/dars/splice-dso-governance-0.1.29.dar differ diff --git a/daml/dars/splice-test-token-v1-1.0.1.dar b/daml/dars/splice-test-token-v1-1.0.1.dar new file mode 100644 index 00000000000..97d176c20dd Binary files /dev/null and b/daml/dars/splice-test-token-v1-1.0.1.dar differ diff --git a/daml/dars/splice-test-token-v2-1.0.1.dar b/daml/dars/splice-test-token-v2-1.0.1.dar new file mode 100644 index 00000000000..8201cecbb79 Binary files /dev/null and b/daml/dars/splice-test-token-v2-1.0.1.dar differ diff --git a/daml/dars/splice-token-standard-utils-2.0.0.dar b/daml/dars/splice-token-standard-utils-2.0.0.dar index c39d1ef6411..6b06b93a1b3 100644 Binary files a/daml/dars/splice-token-standard-utils-2.0.0.dar and b/daml/dars/splice-token-standard-utils-2.0.0.dar differ diff --git a/daml/dars/splice-token-test-dummy-holding-0.0.5.dar b/daml/dars/splice-token-test-dummy-holding-0.0.5.dar new file mode 100644 index 00000000000..7b1f7da48bf Binary files /dev/null and b/daml/dars/splice-token-test-dummy-holding-0.0.5.dar differ diff --git a/daml/dars/splice-token-test-trading-app-1.0.3.dar b/daml/dars/splice-token-test-trading-app-1.0.3.dar new file mode 100644 index 00000000000..556a68876a5 Binary files /dev/null and b/daml/dars/splice-token-test-trading-app-1.0.3.dar differ diff --git a/daml/dars/splice-token-test-trading-app-v2-1.0.1.dar b/daml/dars/splice-token-test-trading-app-v2-1.0.1.dar new file mode 100644 index 00000000000..d6201c4017f Binary files /dev/null and b/daml/dars/splice-token-test-trading-app-v2-1.0.1.dar differ diff --git a/daml/dars/splice-util-0.1.7.dar b/daml/dars/splice-util-0.1.7.dar index c20682ed188..6f217b8ade8 100644 Binary files a/daml/dars/splice-util-0.1.7.dar and b/daml/dars/splice-util-0.1.7.dar differ diff --git a/daml/dars/splice-util-0.1.8.dar b/daml/dars/splice-util-0.1.8.dar new file mode 100644 index 00000000000..3db99d4dc17 Binary files /dev/null and b/daml/dars/splice-util-0.1.8.dar differ diff --git a/daml/dars/splice-util-batched-markers-1.0.4.dar b/daml/dars/splice-util-batched-markers-1.0.4.dar new file mode 100644 index 00000000000..a31ec0794a9 Binary files /dev/null and b/daml/dars/splice-util-batched-markers-1.0.4.dar differ diff --git a/daml/dars/splice-util-token-standard-wallet-1.1.1.dar b/daml/dars/splice-util-token-standard-wallet-1.1.1.dar new file mode 100644 index 00000000000..5ae804665a4 Binary files /dev/null and b/daml/dars/splice-util-token-standard-wallet-1.1.1.dar differ diff --git a/daml/dars/splice-validator-lifecycle-0.1.9.dar b/daml/dars/splice-validator-lifecycle-0.1.9.dar new file mode 100644 index 00000000000..2b21ba63465 Binary files /dev/null and b/daml/dars/splice-validator-lifecycle-0.1.9.dar differ diff --git a/daml/dars/splice-wallet-0.1.24.dar b/daml/dars/splice-wallet-0.1.24.dar new file mode 100644 index 00000000000..8e79fe2d2d6 Binary files /dev/null and b/daml/dars/splice-wallet-0.1.24.dar differ diff --git a/daml/dars/splice-wallet-payments-0.1.23.dar b/daml/dars/splice-wallet-payments-0.1.23.dar new file mode 100644 index 00000000000..26b755ad5cd Binary files /dev/null and b/daml/dars/splice-wallet-payments-0.1.23.dar differ diff --git a/daml/dars/splitwell-0.1.24.dar b/daml/dars/splitwell-0.1.24.dar new file mode 100644 index 00000000000..4f6b8ca971a Binary files /dev/null and b/daml/dars/splitwell-0.1.24.dar differ diff --git a/daml/splice-amulet-name-service-test/daml.yaml b/daml/splice-amulet-name-service-test/daml.yaml index b92a89321dd..246afc7dd9e 100644 --- a/daml/splice-amulet-name-service-test/daml.yaml +++ b/daml/splice-amulet-name-service-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-amulet-name-service-test source: daml -version: 0.1.27 +version: 0.1.28 dependencies: - daml-prim - daml-stdlib @@ -16,6 +16,7 @@ data-dependencies: - ../../token-standard/splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar - ../../token-standard/splice-token-standard-v2-test/.daml/dist/splice-token-standard-v2-test-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-amulet-name-service/daml.yaml b/daml/splice-amulet-name-service/daml.yaml index 469fb444570..c874fb70bbc 100644 --- a/daml/splice-amulet-name-service/daml.yaml +++ b/daml/splice-amulet-name-service/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-amulet-name-service source: daml -version: 0.1.23 +version: 0.1.24 dependencies: - daml-prim - daml-stdlib @@ -11,6 +11,7 @@ data-dependencies: - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar - ../dars/splice-api-featured-app-v1-1.0.0.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans.daml b/daml/splice-amulet-name-service/daml/Splice/Ans.daml index 43564af0c65..f38b4ba87df 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans.daml @@ -17,44 +17,52 @@ data AnsRules_RequestEntryResult = AnsRules_RequestEntryResult with entryCid : ContractId AnsEntryContext requestCid : ContractId SubscriptionRequest + deriving (Serializable) data AnsRules_CollectInitialEntryPaymentResult = AnsRules_CollectInitialEntryPaymentResult with entryCid : ContractId AnsEntry subscriptionStateCid : ContractId SubscriptionIdleState + deriving (Serializable) data AnsEntryContext_CollectInitialEntryPaymentResult = AnsEntryContext_CollectInitialEntryPaymentResult with entryCid : ContractId AnsEntry subscriptionStateCid : ContractId SubscriptionIdleState + deriving (Serializable) data AnsRules_CollectEntryRenewalPaymentResult = AnsRules_CollectEntryRenewalPaymentResult with entryCid : ContractId AnsEntry subscriptionStateCid : ContractId SubscriptionIdleState + deriving (Serializable) data AnsRules_RejectEntryInitialPaymentResult = AnsRules_RejectEntryInitialPaymentResult with amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data AnsEntryContext_CollectEntryRenewalPaymentResult = AnsEntryContext_CollectEntryRenewalPaymentResult with entryCid : ContractId AnsEntry subscriptionStateCid : ContractId SubscriptionIdleState + deriving (Serializable) data AnsEntryContext_RejectEntryInitialPaymentResult = AnsEntryContext_RejectEntryInitialPaymentResult with amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data AnsEntryContext_TerminateResult = AnsEntryContext_TerminateResult with - + deriving (Serializable) data AnsEntry_ExpireResult = AnsEntry_ExpireResult with - + deriving (Serializable) data AnsEntry_RenewResult = AnsEntry_RenewResult with entryCid : ContractId AnsEntry + deriving (Serializable) -- | The rules governing how users can pay to use the Amulet Name service. template AnsRules @@ -174,7 +182,7 @@ data ExpectedPayment = ExpectedPayment with dso : Party sender : Party - deriving (Show, Eq) + deriving (Show, Eq, Serializable) fetchAndValidateInitialPayment : ContractId SubscriptionInitialPayment -> ExpectedPayment -> Update SubscriptionInitialPayment fetchAndValidateInitialPayment cid ExpectedPayment{..} = do @@ -197,7 +205,7 @@ data ExpectedEntryContext = ExpectedEntryContext dso : Party user : Party reference : ContractId SubscriptionRequest - deriving (Show, Eq) + deriving (Show, Eq, Serializable) fetchAndValidateEntryContext : ContractId AnsEntryContext -> ExpectedEntryContext -> Update AnsEntryContext fetchAndValidateEntryContext cid ExpectedEntryContext{..} = do @@ -212,7 +220,7 @@ data AnsRulesConfig = AnsRulesConfig with entryLifetime : RelTime -- Duration until an entry expires entryFee : Decimal -- Fee for each entry descriptionPrefix : Text -- Prefix to use for the description of the entry subscription - deriving (Eq, Show) + deriving (Eq, Show, Serializable) template AnsEntryContext with diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml b/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml index 2993386a347..bea2a4a4a10 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml @@ -24,7 +24,7 @@ data MarkerContext = MarkerContext with featuredAppRightCid : ContractId Api.FeaturedAppRight beneficiaries : [Api.AppRewardBeneficiary] - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Amulet conversion rate feed published by a given 'publisher'. -- Publisher must only create one feed per publisher. @@ -75,7 +75,9 @@ instance HasCheckedFetch Api.FeaturedAppRightView ForOwner where contractGroupId Api.FeaturedAppRightView{..} = ForOwner{dso,owner = provider} data AmuletConversionRateFeed_ArchiveAsDsoResult = AmuletConversionRateFeed_ArchiveAsDsoResult + deriving (Serializable) data AmuletConversionRateFeed_UpdateResult = AmuletConversionRateFeed_UpdateResult with cid : ContractId AmuletConversionRateFeed + deriving (Serializable) diff --git a/daml/splice-amulet-test/daml.yaml b/daml/splice-amulet-test/daml.yaml index 60b847b0476..171084c4a45 100644 --- a/daml/splice-amulet-test/daml.yaml +++ b/daml/splice-amulet-test/daml.yaml @@ -6,7 +6,7 @@ sdk-version: 3.5.2 name: splice-amulet-test source: daml -version: 0.1.26 +version: 0.1.27 dependencies: - daml-prim - daml-stdlib @@ -34,6 +34,7 @@ data-dependencies: - ../../token-standard/splice-token-standard-v1-test/.daml/dist/splice-token-standard-v1-test-current.dar - ../../token-standard/splice-token-standard-v2-test/.daml/dist/splice-token-standard-v2-test-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - -Wno-template-interface-depends-on-daml-script diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml index f35e6603d30..58462fd4b32 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml @@ -637,7 +637,7 @@ testInputDevelopmentFundCoupon = do reason = "Bob fixed issue XXX" AmuletRules_AllocateDevelopmentFundCouponResult { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- - allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty totalAmount expiresAt reason [cid1, cid2] + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty totalAmount expiresAt reason [cid1, cid2] None -- Unhappy - expiresAt has been reached setTime $ addRelTime expiresAt (minutes 1) diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml index be635c8e4c5..f623af4e15a 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml @@ -36,32 +36,49 @@ testAllocateDevelopmentFundCoupon = do reason = "Bob fixed issue XXX" firstAllocationAmount = 10.0 secondAllocationAmount = totalAmount - firstAllocationAmount + -- Kept far shorter than the delay used on production, so that the coupons allocated + -- below stay mintable well before they expire. + minMintingDelay = minutes 1 + mintAfter = addRelTime now (minutes 2) -- Unhappy - DevelopmentFundCoupon cannot be allocated without a Development Fund manager configured allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount - expiresAt reason unclaimedDevelopmentFundCouponCids + expiresAt reason unclaimedDevelopmentFundCouponCids (Some mintAfter) -- Set 5% and Alice as the fund manager replaceDevelopmentFundConfig app baseConfig (Some alice.primaryParty) (Some 0.05) + setMinDevelopmentFundMintingDelay app minMintingDelay -- Unhappy - expiresAt is in the past let invalidExpiresAt = addRelTime now (minutes (-1)) allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount - invalidExpiresAt reason unclaimedDevelopmentFundCouponCids + invalidExpiresAt reason unclaimedDevelopmentFundCouponCids (Some mintAfter) -- Unhappy - controller must be the configured Development Fund manager (alice) allocateDevelopmentFundCouponMustFail app bob.primaryParty bob.primaryParty firstAllocationAmount - expiresAt reason unclaimedDevelopmentFundCouponCids + expiresAt reason unclaimedDevelopmentFundCouponCids (Some mintAfter) + + -- Unhappy - a mintAfter time is required once a minimum minting delay is configured + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount + expiresAt reason unclaimedDevelopmentFundCouponCids None + + -- Unhappy - mintAfter is not at least minDevelopmentFundMintingDelay in the future + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount + expiresAt reason unclaimedDevelopmentFundCouponCids (Some (addRelTime now (seconds 30))) + + -- Unhappy - mintAfter is not before expiresAt + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount + expiresAt reason unclaimedDevelopmentFundCouponCids (Some expiresAt) -- Unhappy - insufficient amount to cover the requested allocation allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty (totalAmount + 1.0) - expiresAt reason unclaimedDevelopmentFundCouponCids + expiresAt reason unclaimedDevelopmentFundCouponCids (Some mintAfter) -- Happy - Development Fund coupon allocated with UnclaimedDevelopmentFundCoupon leftover AmuletRules_AllocateDevelopmentFundCouponResult { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = Some leftOverUnclaimedDevelopmentFundCouponCid } <- allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty firstAllocationAmount - expiresAt reason unclaimedDevelopmentFundCouponCids + expiresAt reason unclaimedDevelopmentFundCouponCids (Some mintAfter) Some developmentFundCoupon <- queryContractId bob.primaryParty developmentFundCouponCid Some leftOverUnclaimedDevelopmentFundCoupon <- queryContractId app.dso leftOverUnclaimedDevelopmentFundCouponCid developmentFundCoupon === DevelopmentFundCoupon with @@ -71,6 +88,7 @@ testAllocateDevelopmentFundCoupon = do amount = firstAllocationAmount expiresAt reason + mintAfter = Some mintAfter leftOverUnclaimedDevelopmentFundCoupon === UnclaimedDevelopmentFundCoupon with dso = app.dso amount = secondAllocationAmount @@ -78,7 +96,7 @@ testAllocateDevelopmentFundCoupon = do -- Happy - Development Fund coupon allocated with no UnclaimedDevelopmentFundCoupon leftover AmuletRules_AllocateDevelopmentFundCouponResult { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty secondAllocationAmount - expiresAt reason [leftOverUnclaimedDevelopmentFundCouponCid] + expiresAt reason [leftOverUnclaimedDevelopmentFundCouponCid] (Some mintAfter) Some developmentFundCoupon <- queryContractId bob.primaryParty developmentFundCouponCid developmentFundCoupon === DevelopmentFundCoupon with dso = app.dso @@ -87,6 +105,7 @@ testAllocateDevelopmentFundCoupon = do amount = secondAllocationAmount expiresAt reason + mintAfter = Some mintAfter pure () @@ -185,14 +204,208 @@ testExpiryOfDevelopmentFundCoupon = do pure () +testDevelopmentFundCouponMintingDelay : Script () +testDevelopmentFundCouponMintingDelay = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + baseConfig <- getAmuletConfig app + replaceDevelopmentFundConfig app baseConfig (Some alice.primaryParty) (Some 0.05) + setMinDevelopmentFundMintingDelay app (minutes 1) + + -- Run 3 issuances for the already open rounds without the development fund configuration. + replicateA_ 3 $ runNextIssuance app + -- Mint one development fund coupon per issuance (two total). + runNextIssuance app + runNextIssuance app + + [(cid1, coupon1), (cid2, coupon2)] <- query @UnclaimedDevelopmentFundCoupon app.dso + now <- getTime + let + totalAmount = coupon1.amount + coupon2.amount + unclaimedDevelopmentFundCouponCids = [cid1, cid2] + expiresAt = addRelTime now (days 2) + reason = "Bob fixed issue XXX" + mintAfter = addRelTime now (minutes 2) + + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty totalAmount + expiresAt reason unclaimedDevelopmentFundCouponCids (Some mintAfter) + Some developmentFundCoupon <- queryContractId bob.primaryParty developmentFundCouponCid + developmentFundCoupon.mintAfter === Some mintAfter + + -- Unhappy - the coupon cannot be minted before mintAfter + mintDevelopmentFundCouponMustFail app bob developmentFundCouponCid + + -- Happy - the coupon can be minted once mintAfter has passed + passTime (minutes 3) + transferResult <- mintDevelopmentFundCoupon app bob developmentFundCouponCid + transferResult.summary.inputDevelopmentFundAmount === Some totalAmount + + pure () + +-- | Coupons allocated with no `mintAfter` prior to we introduce minimum minting delay stay +-- immediately mintable. +testDevelopmentFundCouponWithoutMintingDelay : Script () +testDevelopmentFundCouponWithoutMintingDelay = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + baseConfig <- getAmuletConfig app + replaceDevelopmentFundConfig app baseConfig (Some alice.primaryParty) (Some 0.05) + + -- Run 3 issuances for the already open rounds without the development fund configuration. + replicateA_ 3 $ runNextIssuance app + -- Mint one development fund coupon. + runNextIssuance app + + [(cid1, coupon1)] <- query @UnclaimedDevelopmentFundCoupon app.dso + now <- getTime + let + expiresAt = addRelTime now (days 2) + reason = "Bob fixed issue XXX" + + -- Unhappy - a mintAfter in the past is rejected even with no minimum minting delay configured + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty coupon1.amount + expiresAt reason [cid1] (Some (addRelTime now (minutes (-1)))) + + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty coupon1.amount + expiresAt reason [cid1] None + Some developmentFundCoupon <- queryContractId bob.primaryParty developmentFundCouponCid + developmentFundCoupon.mintAfter === None + + -- Happy - and the coupon mints immediately, even after a delay has been configured + setMinDevelopmentFundMintingDelay app (days 7) + transferResult <- mintDevelopmentFundCoupon app bob developmentFundCouponCid + transferResult.summary.inputDevelopmentFundAmount === Some coupon1.amount + + pure () + +testDevelopmentFundManagerBlacklist : Script () +testDevelopmentFundManagerBlacklist = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + baseConfig <- getAmuletConfig app + replaceDevelopmentFundConfig app baseConfig (Some alice.primaryParty) (Some 0.05) + + -- Run 3 issuances for the already open rounds without the development fund configuration. + replicateA_ 3 $ runNextIssuance app + -- Mint one development fund coupon per issuance (three total). + replicateA_ 3 $ runNextIssuance app + + [(cid1, coupon1), (cid2, coupon2), (cid3, coupon3)] <- query @UnclaimedDevelopmentFundCoupon app.dso + now <- getTime + let + expiresAt = addRelTime now (days 2) + reason = "Bob fixed issue XXX" + -- Must exceed `transferPreapprovalBaseDuration` so creating the pre-approval + -- burns amulet via `splitAndBurn`. + preapprovalExpiresAt = addRelTime now (days 100) + + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid = transferCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty coupon1.amount + expiresAt reason [cid1] None + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid = preapprovalCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty coupon2.amount + expiresAt reason [cid2] None + + setDevelopmentFundManagerBlacklist app [alice.primaryParty] + + -- Unhappy - a blacklisted fund manager cannot allocate further coupons + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty coupon3.amount + expiresAt reason [cid3] None + + -- Unhappy - an already allocated coupon cannot be minted through AmuletRules_Transfer + mintDevelopmentFundCouponMustFail app bob transferCouponCid + + -- Unhappy - nor through the splitAndBurn path used to pay for a transfer pre-approval + context <- getPaymentTransferContext app bob + submitMustFail (actAs bob.primaryParty <> readAs app.dso) $ + exerciseCmd context.amuletRules AmuletRules_CreateTransferPreapproval with + context + inputs = [InputDevelopmentFundCoupon preapprovalCouponCid] + receiver = bob.primaryParty + provider = bob.primaryParty + expiresAt = preapprovalExpiresAt + expectedDso = Some app.dso + optCreatedAt = None + + setDevelopmentFundManagerBlacklist app [] + + -- Happy - coupon allocated + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid = expiringCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty coupon3.amount + expiresAt reason [cid3] None + + -- Happy - coupon minted + transferResult <- mintDevelopmentFundCoupon app bob transferCouponCid + transferResult.summary.inputDevelopmentFundAmount === Some coupon1.amount + + -- Happy - pre-approval coupon minted + context <- getPaymentTransferContext app bob + _ <- submit (actAs bob.primaryParty <> readAs app.dso) $ + exerciseCmd context.amuletRules AmuletRules_CreateTransferPreapproval with + context + inputs = [InputDevelopmentFundCoupon preapprovalCouponCid] + receiver = bob.primaryParty + provider = bob.primaryParty + expiresAt = preapprovalExpiresAt + expectedDso = Some app.dso + optCreatedAt = None + + -- Happy - the DSO party reclaims an expired coupon from a blacklisted manager + setDevelopmentFundManagerBlacklist app [alice.primaryParty] + passTime (days 3) + _ <- expiresDevelopmentFundCoupon app.dso expiringCouponCid + + pure () + +-- | External party context cannot be used with `DevelopmentFundCoupon` as of now. The test will +-- pass once we add the support for external party context and configure the blacklisted fund managers. +testDevelopmentFundCouponRejectedForExternalParty : Script () +testDevelopmentFundCouponRejectedForExternalParty = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + now <- getTime + let + expiresAt = addRelTime now (days 1) + transferAmount = 10.0 + + setDevelopmentFundManagerBlacklist app [alice.primaryParty] + developmentFundCouponCid <- createDevelopmentFundCoupon app alice.primaryParty bob.primaryParty 50.0 + expiresAt "Bob fixed issue XXX" + + transferPreapprovalCid <- submit + (actAs alice.primaryParty <> actAs aliceValidator.primaryParty <> actAs app.dso) $ + createCmd TransferPreapproval with + receiver = alice.primaryParty + provider = aliceValidator.primaryParty + dso = app.dso + validFrom = now + lastRenewedAt = now + expiresAt + externalContext <- getExternalPartyTransferContext app + + -- Unhappy - the coupon cannot be minted where the blacklist is unknown + submitMustFail (actAs bob.primaryParty <> readAs app.dso) $ + exerciseCmd transferPreapprovalCid TransferPreapproval_SendV2 with + sender = bob.primaryParty + context = externalContext + inputs = [InputDevelopmentFundCoupon developmentFundCouponCid] + amount = transferAmount + description = None + meta = None + + pure () + -- Helpers ---------- allocateDevelopmentFundCouponMustFail : AmuletApp -> Party -> Party -> Decimal -> Time -> Text -> [ContractId UnclaimedDevelopmentFundCoupon] - -> Script () -allocateDevelopmentFundCouponMustFail app fundManager beneficiary amount expiresAt reason unclaimedDevelopmentFundCouponCids = do + -> Optional Time -> Script () +allocateDevelopmentFundCouponMustFail app fundManager beneficiary amount expiresAt reason unclaimedDevelopmentFundCouponCids mintAfter = do [(amuletRulesCid, _)] <- query @AmuletRules app.dso submitMustFail (actAs fundManager <> readAs app.dso) do exerciseCmd amuletRulesCid AmuletRules_AllocateDevelopmentFundCoupon with @@ -202,6 +415,32 @@ allocateDevelopmentFundCouponMustFail app fundManager beneficiary amount expires expiresAt reason fundManager + mintAfter + +mintDevelopmentFundCoupon : AmuletApp -> AmuletUser -> ContractId DevelopmentFundCoupon -> Script TransferResult +mintDevelopmentFundCoupon app beneficiary developmentFundCouponCid = do + context <- getPaymentTransferContext app beneficiary + submit (actAs beneficiary.primaryParty <> readAs app.dso) $ + exerciseCmd context.amuletRules (mintDevelopmentFundCouponTransfer app beneficiary context developmentFundCouponCid) + +mintDevelopmentFundCouponMustFail : AmuletApp -> AmuletUser -> ContractId DevelopmentFundCoupon -> Script () +mintDevelopmentFundCouponMustFail app beneficiary developmentFundCouponCid = do + context <- getPaymentTransferContext app beneficiary + submitMustFail (actAs beneficiary.primaryParty <> readAs app.dso) $ + exerciseCmd context.amuletRules (mintDevelopmentFundCouponTransfer app beneficiary context developmentFundCouponCid) + +mintDevelopmentFundCouponTransfer + : AmuletApp -> AmuletUser -> PaymentTransferContext -> ContractId DevelopmentFundCoupon -> AmuletRules_Transfer +mintDevelopmentFundCouponTransfer app beneficiary context developmentFundCouponCid = + AmuletRules_Transfer with + transfer = Transfer with + sender = beneficiary.primaryParty + provider = beneficiary.primaryParty + inputs = [InputDevelopmentFundCoupon developmentFundCouponCid] + outputs = [] + beneficiaries = None + context = context.context + expectedDso = Some app.dso createDevelopmentFundCoupon : AmuletApp -> Party -> Party -> Decimal -> Time -> Text -> Script (ContractId DevelopmentFundCoupon) createDevelopmentFundCoupon app fundManager beneficiary amount expiresAt reason = @@ -213,6 +452,7 @@ createDevelopmentFundCoupon app fundManager beneficiary amount expiresAt reason amount expiresAt reason + mintAfter = None rejectDevelopmentFundCoupon : Party -> Text -> ContractId DevelopmentFundCoupon -> Script (ContractId UnclaimedDevelopmentFundCoupon) rejectDevelopmentFundCoupon fundManager reason developmentFundCouponCid = diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml index 84f7673aa1d..e4fcb660e3f 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml @@ -33,9 +33,12 @@ scaleAmuletConfig amuletPrice config = AmuletConfig with transferPreapprovalFee = fmap (/ amuletPrice) config.transferPreapprovalFee featuredAppActivityMarkerAmount = fmap (/ amuletPrice) config.featuredAppActivityMarkerAmount optDevelopmentFundManager = config.optDevelopmentFundManager + developmentFundManagerBlacklist = config.developmentFundManagerBlacklist externalPartyConfigStateTickDuration = config.externalPartyConfigStateTickDuration rewardConfig = config.rewardConfig transferPreapprovalBaseDuration = config.transferPreapprovalBaseDuration + minDevelopmentFundMintingDelay = config.minDevelopmentFundMintingDelay + amuletSwitchOverTimes = config.amuletSwitchOverTimes test : Script () test = script do diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml index 5014a73d7d5..25e82a48786 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml @@ -231,6 +231,7 @@ test_reward_accounting_v2_only_archive_dry_run = do data SetupConfig = SetupConfig with hideCoupon : Bool -- ^ Whether to hide alice's coupon useTrafficBasedAppRewards : Bool -- ^ Whether to configure traffic-based rewards for minting + deriving (Serializable) setupAliceWithCoupon : Bool -> Script (AmuletApp, Party, AmuletUser) setupAliceWithCoupon hideCoupon = setupAliceWithCoupon' $ SetupConfig with diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TokenStandard/TestAmuletAllocationNegative.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TokenStandard/TestAmuletAllocationNegative.daml index b4e70145bc4..21c026f8bbc 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TokenStandard/TestAmuletAllocationNegative.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TokenStandard/TestAmuletAllocationNegative.daml @@ -453,6 +453,7 @@ data SettleTestEnv = SettleTestEnv with aliceAllocCid : ContractId V2.Allocation bobAllocCid : ContractId V2.Allocation bobAllocationV2 : V2.AllocationSpecification + deriving (Serializable) test_settleBatchV2_wrongActor : Script () test_settleBatchV2_wrongActor = test_settleBatchV2_generic $ \SettleTestEnv {..} TestEnv {..} -> do diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml index 095ffc2f3c7..558c1611d78 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml @@ -15,19 +15,19 @@ data RecV1 = RecV1 with a : Int b : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data RecV1' = RecV1' with a : Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data RecV2 = RecV2 with a : Int b : Text c : Optional Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data RecV3 = RecV3 with @@ -35,7 +35,7 @@ data RecV3 = RecV3 b : Text c : Optional Int d : Optional Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance Hashable RecV1 where hash r = hashRecord [hash r.a , hash r.b] @@ -70,7 +70,7 @@ data Payload = Payload with x : Int y : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -------------------------------------------------------------------------------- @@ -80,7 +80,7 @@ data Payload = Payload data VarV1 = V1 with payload : Payload | V2 with payload : Payload - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance Hashable VarV1 where hash v = @@ -111,7 +111,7 @@ data VarV2 payload : Payload c : Optional Int d : Optional Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance Hashable VarV2 where hash v = diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml index 2cef301597b..c986f3b72f5 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml @@ -130,6 +130,16 @@ replaceDevelopmentFundConfig app baseConfig optDevelopmentFundManager optDevelop initialValue = baseConfig.issuanceCurve.initialValue with optDevelopmentFundPercentage +setDevelopmentFundManagerBlacklist : AmuletApp -> [Party] -> Script () +setDevelopmentFundManagerBlacklist app fundManagerBlacklist = + updateAmuletConfig app $ \config -> config with + developmentFundManagerBlacklist = Some fundManagerBlacklist + +setMinDevelopmentFundMintingDelay : AmuletApp -> RelTime -> Script () +setMinDevelopmentFundMintingDelay app delay = + updateAmuletConfig app $ \config -> config with + minDevelopmentFundMintingDelay = Some delay + -- AmuletApp users -------------- @@ -711,8 +721,8 @@ updateAmuletConfig app f = do allocateDevelopmentFundCoupon : AmuletApp -> Party -> Party -> Decimal -> Time -> Text -> [ContractId UnclaimedDevelopmentFundCoupon] - -> Script AmuletRules_AllocateDevelopmentFundCouponResult -allocateDevelopmentFundCoupon app fundManager beneficiary amount expiresAt reason unclaimedDevelopmentFundCouponCids = do + -> Optional Time -> Script AmuletRules_AllocateDevelopmentFundCouponResult +allocateDevelopmentFundCoupon app fundManager beneficiary amount expiresAt reason unclaimedDevelopmentFundCouponCids mintAfter = do submitExerciseAmuletRulesByKey app [fundManager] [] AmuletRules_AllocateDevelopmentFundCoupon with unclaimedDevelopmentFundCouponCids beneficiary @@ -720,6 +730,7 @@ allocateDevelopmentFundCoupon app fundManager beneficiary amount expiresAt reaso expiresAt reason fundManager + mintAfter -- Metadata verification ------------------------ diff --git a/daml/splice-amulet/daml.yaml b/daml/splice-amulet/daml.yaml index 1fa48a1ad1b..ba49f1e3c34 100644 --- a/daml/splice-amulet/daml.yaml +++ b/daml/splice-amulet/daml.yaml @@ -6,7 +6,7 @@ sdk-version: 3.5.2 name: splice-amulet source: daml -version: 0.1.22 +version: 0.1.23 dependencies: - daml-prim - daml-stdlib @@ -27,6 +27,7 @@ data-dependencies: - ../dars/splice-api-featured-app-v2-1.0.0.dar - ../splice-api-reward-assignment-v1/.daml/dist/splice-api-reward-assignment-v1-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-amulet/daml/Splice/Amulet.daml b/daml/splice-amulet/daml/Splice/Amulet.daml index 2473529d5b6..5a636507ff3 100644 --- a/daml/splice-amulet/daml/Splice/Amulet.daml +++ b/daml/splice-amulet/daml/Splice/Amulet.daml @@ -39,12 +39,12 @@ data AmuletExpireSummary = AmuletExpireSummary with -- a amulet created in round 3 is treated as a amulet created in round 0 with a higher initial amount. changeToHoldingFeesRate : Decimal -- ^ The change of total holding fees introduced by a amulet expiry. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AmuletExpireV2Summary = AmuletExpireV2Summary with owner : Party - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Result of an operation that created a new amulet, e.g., -- by minting a fresh amulet, or by unlocking a locked amulet. @@ -52,78 +52,100 @@ data AmuletCreateSummary amuletContractId = AmuletCreateSummary with amulet : amuletContractId -- ^ The new amulet that was created amuletPrice : Decimal -- ^ The amulet price at the round the amulet was created round: Round -- ^ Round for which this amulet was created. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data Amulet_ExpireResult = Amulet_ExpireResult with expireSum : AmuletExpireSummary meta : Optional Api.Token.MetadataV1.Metadata + deriving (Serializable) data Amulet_ExpireV2Result = Amulet_ExpireV2Result with expireSum : AmuletExpireV2Summary meta : Api.Token.MetadataV1.Metadata + deriving (Serializable) data LockedAmulet_UnlockResult = LockedAmulet_UnlockResult with amuletSum : AmuletCreateSummary (ContractId Amulet) meta : Optional Api.Token.MetadataV1.Metadata + deriving (Serializable) data LockedAmulet_UnlockV2Result = LockedAmulet_UnlockV2Result with amuletCid : ContractId Amulet meta : Api.Token.MetadataV1.Metadata + deriving (Serializable) data LockedAmulet_OwnerExpireLockResult = LockedAmulet_OwnerExpireLockResult with amuletSum : AmuletCreateSummary (ContractId Amulet) meta : Optional Api.Token.MetadataV1.Metadata + deriving (Serializable) data LockedAmulet_OwnerExpireLockV2Result = LockedAmulet_OwnerExpireLockV2Result with amuletCid : ContractId Amulet meta : Api.Token.MetadataV1.Metadata + deriving (Serializable) data LockedAmulet_ExpireAmuletResult = LockedAmulet_ExpireAmuletResult with expireSum : AmuletExpireSummary meta : Optional Api.Token.MetadataV1.Metadata + deriving (Serializable) data LockedAmulet_ExpireAmuletV2Result = LockedAmulet_ExpireAmuletV2Result with expireSum : AmuletExpireV2Summary meta : Api.Token.MetadataV1.Metadata + deriving (Serializable) data ValidatorRight_ArchiveAsValidatorResult = ValidatorRight_ArchiveAsValidatorResult + deriving (Serializable) data ValidatorRight_ArchiveAsUserResult = ValidatorRight_ArchiveAsUserResult + deriving (Serializable) data FeaturedAppRight_WithdrawResult = FeaturedAppRight_WithdrawResult + deriving (Serializable) data FeaturedAppRight_UpdateResult = FeaturedAppRight_UpdateResult with featuredAppRightCid : ContractId FeaturedAppRight + deriving (Serializable) data FeaturedAppRight_CancelResult = FeaturedAppRight_CancelResult + deriving (Serializable) data AppRewardCoupon_DsoExpireResult = AppRewardCoupon_DsoExpireResult with featured : Bool amount : Decimal + deriving (Serializable) data ValidatorRewardCoupon_DsoExpireResult = ValidatorRewardCoupon_DsoExpireResult with amount : Decimal + deriving (Serializable) data ValidatorRewardCoupon_ArchiveAsValidatorResult = ValidatorRewardCoupon_ArchiveAsValidatorResult with - + deriving (Serializable) data SvRewardCoupon_DsoExpireResult = SvRewardCoupon_DsoExpireResult with weight: Int + deriving (Serializable) data SvRewardCoupon_ArchiveAsBeneficiaryResult = SvRewardCoupon_ArchiveAsBeneficiaryResult + deriving (Serializable) data UnclaimedActivityRecord_ArchiveAsBeneficiaryResult = UnclaimedActivityRecord_ArchiveAsBeneficiaryResult + deriving (Serializable) data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid : ContractId UnclaimedReward + deriving (Serializable) data DevelopmentFundCoupon_WithdrawResult = DevelopmentFundCoupon_WithdrawResult with unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + deriving (Serializable) data DevelopmentFundCoupon_RejectResult = DevelopmentFundCoupon_RejectResult with unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + deriving (Serializable) data DevelopmentFundCoupon_DsoExpireResult = DevelopmentFundCoupon_DsoExpireResult with unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + deriving (Serializable) -- | A amulet, which can be locked and whose amount expires over time. -- @@ -548,8 +570,9 @@ template DevelopmentFundCoupon amount : Decimal -- ^ The total amount of `Amulet` to mint on collection. expiresAt : Time -- ^ Until when the minting can be completed. reason : Text -- ^ Reason for the emission of the coupon. + mintAfter : Optional Time -- ^ The time after which the coupon can be minted. where - ensure amount > 0.0 + ensure amount > 0.0 && optional True (< expiresAt) mintAfter signatory dso diff --git a/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml b/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml index 951b096c225..f58fbcb9f8d 100644 --- a/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml +++ b/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml @@ -23,7 +23,7 @@ import DA.Optional (isNone) import DA.Text qualified as T data Hash = Hash with value : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Compute the hash of a record. hashRecord : [Hash] -> Hash diff --git a/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml b/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml index 00448eb0e35..fe5c4e753c3 100644 --- a/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml +++ b/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml @@ -45,17 +45,17 @@ template CalculateRewardsV2 with data MintingAllowance = MintingAllowance with provider : Party amount : Decimal - deriving (Eq, Show) + deriving (Eq, Show, Serializable) type MintingAllowances = [MintingAllowance] data Batch = BatchOfBatches [CryptoHash.Hash] | BatchOfMintingAllowances MintingAllowances - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data ProcessRewardsV2_ProcessBatchResult = ProcessRewardsV2_ProcessBatchResult {} - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | State contract tracking outstanding processing of rewards for a given round and batch hash. template ProcessRewardsV2 with diff --git a/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml b/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml index e2d14608b8c..7ebb3d799d4 100644 --- a/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml +++ b/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml @@ -139,7 +139,7 @@ data TxKind | TxKind_Burn | TxKind_Mint | TxKind_ExpireDust - deriving (Eq) -- intentionally no Show to avoid SCU contraints on code changes + deriving (Eq, Serializable) -- helper instances: defined here as they are not part of the API instance Semigroup Metadata where diff --git a/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml b/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml index ec91e2fed64..aad83d5fb19 100644 --- a/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml +++ b/daml/splice-amulet/daml/Splice/Amulet/TwoStepTransfer.daml @@ -44,6 +44,7 @@ data TwoStepTransfer = TwoStepTransfer with -- ^ Provider that should be marked as the app provider on the second step. allowFeaturing : Bool -- ^ Whether the second step can be featured. + deriving (Serializable) -- | Converting a set of holding inputs to inputs for an amulet transfer, -- unlocking any expired LockedAmulet holdings on the fly. diff --git a/daml/splice-amulet/daml/Splice/AmuletAllocation.daml b/daml/splice-amulet/daml/Splice/AmuletAllocation.daml index 42b1423ca44..343171a3563 100644 --- a/daml/splice-amulet/daml/Splice/AmuletAllocation.daml +++ b/daml/splice-amulet/daml/Splice/AmuletAllocation.daml @@ -35,7 +35,7 @@ data AmuletAllocation_DsoExpireResult = AmuletAllocation_DsoExpireResult with senderHoldingCids : [ContractId V1.Holding] meta : Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | V1 amulet allocation, which was created using a V1 allocation factory and -- can be settled on its own using `V1.Allocation_ExecuteTransfer` or by diff --git a/daml/splice-amulet/daml/Splice/AmuletConfig.daml b/daml/splice-amulet/daml/Splice/AmuletConfig.daml index cbb735b2f56..cfce5b144ca 100644 --- a/daml/splice-amulet/daml/Splice/AmuletConfig.daml +++ b/daml/splice-amulet/daml/Splice/AmuletConfig.daml @@ -5,6 +5,7 @@ module Splice.AmuletConfig where import DA.Optional import DA.Time +import DA.TextMap import Splice.Fees import Splice.DecentralizedSynchronizer @@ -13,9 +14,9 @@ import Splice.Schedule import Splice.Util -- TODO(#14972): Get rid of the unused Amulet data type -data Amulet = Amulet deriving (Show, Eq) +data Amulet = Amulet deriving (Show, Eq, Serializable) -- ^ Deprecated type for specifying amounts and fees in units of Amulet. Use Splice.Amulet.Amulet directly instead. -data USD = USD deriving (Show, Eq) +data USD = USD deriving (Show, Eq, Serializable) -- | Configuration determining the fees and limits for Amulet transfers granted by -- the AmuletRules. @@ -32,7 +33,7 @@ data TransferConfig unit = TransferConfig with maxNumLockHolders : Int -- ^ Maximum number of lock holders allowed for a locked amulet. tokenStandardMaxTTL : Optional RelTime -- ^ Maximum time to live for token standard state contracts (allocations and transfer instructions). Defaults to 90 days. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) transferConfigToTransferConfigV2 : TransferConfig unit -> TransferConfigV2 unit transferConfigToTransferConfigV2 tc = TransferConfigV2 with @@ -49,7 +50,7 @@ data TransferConfigV2 unit = TransferConfigV2 with maxNumOutputs : Int maxNumLockHolders : Int tokenStandardMaxTTL : Optional RelTime - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Get the TTL for token standard state contracts, falling back to a default if not specified. getTokenStandardMaxTTL : TransferConfigV2 unit -> RelTime @@ -65,7 +66,7 @@ data RewardVersion -- ^ Version of app rewards pre CIP-104 | RewardVersion_TrafficBasedAppRewards -- ^ Traffic-based app rewards as introduced in CIP-104 - deriving (Eq, Show) + deriving (Eq, Show, Serializable) useTrafficBasedAppRewards : Optional RewardVersion -> Bool useTrafficBasedAppRewards rv = case rv of @@ -85,7 +86,7 @@ data RewardConfig = RewardConfig with appRewardCouponThreshold : Decimal -- ^ Threshold for minting reward coupons, in USD. -- This threshold is not enforced in daml, but it must be used while building the Merkle-tree -- to ignore minting allowances lower than this value. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Full AmuletConfig @@ -112,11 +113,20 @@ data AmuletConfig unit = AmuletConfig with rewardConfig : Optional RewardConfig -- ^ Configuration for the reward accounting and calculation. If None, then on-ledger reward tracking is used. transferPreapprovalBaseDuration : Optional RelTime -- ^ Base duration of a transfer pre-approval granted for free on creation and renewal. Default: 90 days. - deriving (Eq, Show) + developmentFundManagerBlacklist : Optional [Party] + -- ^ List of blocked parties. They cannot allocate new coupons, and the coupons they allocated cannot be minted either. + minDevelopmentFundMintingDelay : Optional RelTime + -- ^ Minimum delay between allocation and minting. Default: 0 days. + amuletSwitchOverTimes : Optional (TextMap Time) + -- ^ Timepoints for synchronized switch-overs of amulet functionality both on and off-ledger. + deriving (Eq, Show, Serializable) getExternalPartyConfigStateTickDuration : AmuletConfig a -> RelTime getExternalPartyConfigStateTickDuration config = fromOptional (hours 24) config.externalPartyConfigStateTickDuration +getDevelopmentFundManagerBlacklist : AmuletConfig unit -> [Party] +getDevelopmentFundManagerBlacklist config = fromOptional [] config.developmentFundManagerBlacklist + -- | $1/year specified as a daily rate defaultTransferPreapprovalFee : Decimal defaultTransferPreapprovalFee = 0.00274 @@ -139,7 +149,10 @@ validAmuletConfig AmuletConfig , tickDuration , packageConfig , transferPreapprovalFee - , featuredAppActivityMarkerAmount} + , featuredAppActivityMarkerAmount + , rewardConfig + , minDevelopmentFundMintingDelay + } = validTransferConfig transferConfig && validIssuanceCurve issuanceCurve && @@ -148,7 +161,9 @@ validAmuletConfig AmuletConfig validPackageConfig packageConfig && optional True (>= 0.0) transferPreapprovalFee && optional True (>= 0.0) featuredAppActivityMarkerAmount && - featuredAppActivityMarkerAmount == Some transferConfig.extraFeaturedAppRewardAmount + featuredAppActivityMarkerAmount == Some transferConfig.extraFeaturedAppRewardAmount && + optional True validRewardConfig rewardConfig && + optional True (>= days 0) minDevelopmentFundMintingDelay validTransferConfig : TransferConfig unit -> Bool validTransferConfig TransferConfig @@ -164,7 +179,6 @@ validTransferConfig TransferConfig = createFee == FixedFee 0.0 && lockHolderFee == FixedFee 0.0 && - lockHolderFee == FixedFee 0.0 && transferFee.initialRate == 0.0 && -- TODO(#4424) Enforce `null transferFee.steps` all (\(_step, rate) -> rate == 0.0) transferFee.steps && @@ -174,6 +188,17 @@ validTransferConfig TransferConfig maxNumOutputs >= 1 && maxNumLockHolders >= 0 +validRewardConfig : RewardConfig -> Bool +validRewardConfig (RewardConfig + _mintingVersion + _dryRunVersion + batchSize + rewardCouponTimeToLive + appRewardCouponThreshold) = + batchSize > 0 && + rewardCouponTimeToLive > hours 0 && + appRewardCouponThreshold >= 0.0 + -- | The package config defines for each daml package (identified by name) -- the package version that should be used for command submissions -- at that point. @@ -185,7 +210,7 @@ data PackageConfig = PackageConfig validatorLifecycle : Text wallet : Text walletPayments : Text - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Package configs are not constrained at the Daml level to maximize flexibility. -- In particular, empty packages versions might be used in the future to indicate that @@ -206,9 +231,13 @@ instance Patchable (AmuletConfig USD) where transferPreapprovalFee = patch new.transferPreapprovalFee base.transferPreapprovalFee current.transferPreapprovalFee featuredAppActivityMarkerAmount = patch new.featuredAppActivityMarkerAmount base.featuredAppActivityMarkerAmount current.featuredAppActivityMarkerAmount optDevelopmentFundManager = patch new.optDevelopmentFundManager base.optDevelopmentFundManager current.optDevelopmentFundManager + developmentFundManagerBlacklist = + patchScalar new.developmentFundManagerBlacklist base.developmentFundManagerBlacklist current.developmentFundManagerBlacklist externalPartyConfigStateTickDuration = patch new.externalPartyConfigStateTickDuration base.externalPartyConfigStateTickDuration current.externalPartyConfigStateTickDuration rewardConfig = patch new.rewardConfig base.rewardConfig current.rewardConfig transferPreapprovalBaseDuration = patch new.transferPreapprovalBaseDuration base.transferPreapprovalBaseDuration current.transferPreapprovalBaseDuration + minDevelopmentFundMintingDelay = patch new.minDevelopmentFundMintingDelay base.minDevelopmentFundMintingDelay current.minDevelopmentFundMintingDelay + amuletSwitchOverTimes = patch new.amuletSwitchOverTimes base.amuletSwitchOverTimes current.amuletSwitchOverTimes instance Patchable RewardConfig where patch new base current = RewardConfig with diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index f1292fa96f1..e31cd95b50e 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -13,7 +13,7 @@ import DA.Foldable (forA_) import DA.List (sort, dedupSort, maximumOn) import DA.Map (Map) import qualified DA.Map as Map -import DA.Optional (fromOptional, isNone, isSome, optionalToList) +import DA.Optional (fromOptional, isNone, isSome, optionalToList, whenSome) import DA.Set (Set) import qualified DA.Set as Set import DA.TextMap (TextMap) @@ -37,7 +37,8 @@ import Splice.Amulet.RewardAccountingV2 import Splice.Amulet.TokenApiUtils import Splice.AmuletConfig ( transferConfigToTransferConfigV2, AmuletConfig(..), TransferConfig(..), TransferConfigV2(..), validAmuletConfig - , defaultTransferPreapprovalFee, getExternalPartyConfigStateTickDuration, getTransferPreapprovalBaseDuration, useTrafficBasedAppRewards + , defaultTransferPreapprovalFee, getDevelopmentFundManagerBlacklist, getExternalPartyConfigStateTickDuration + , getTransferPreapprovalBaseDuration, useTrafficBasedAppRewards , RewardVersion(..), useFeaturedAppMarkers ) import qualified Splice.AmuletConfig as Unit @@ -56,93 +57,116 @@ import Splice.Util data AmuletRules_Amulet_ExpireTransferInstructionsResult = AmuletRules_Amulet_ExpireTransferInstructionsResult with results : [TransferInstructionResult] - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AmuletRules_ExpireTransferInstructionInput = AmuletRules_ExpireTransferInstructionInput with transferInstructionCid : ContractId TransferInstruction expireLock : Bool - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AmuletRules_BootstrapExternalPartyConfigStateResult = AmuletRules_BootstrapExternalPartyConfigStateResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data AmuletRules_ComputeFeesResult = AmuletRules_ComputeFeesResult with fees : [Decimal] + deriving (Serializable) data AmuletRules_MergeMemberTrafficContractsResult = AmuletRules_MergeMemberTrafficContractsResult with mergedTrafficCid : ContractId MemberTraffic + deriving (Serializable) data AmuletRules_MintResult = AmuletRules_MintResult with amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data AmuletRules_DevNet_TapResult = AmuletRules_DevNet_TapResult with amuletSum : AmuletCreateSummary (ContractId Amulet) meta : Optional Metadata + deriving (Serializable) data AmuletRules_DevNet_FeatureAppResult = AmuletRules_DevNet_FeatureAppResult with featuredAppRightCid : ContractId FeaturedAppRight + deriving (Serializable) data AmuletRules_Bootstrap_RoundsResult = AmuletRules_Bootstrap_RoundsResult with openMiningRoundCid : ContractId OpenMiningRound initialRound : Optional Round + deriving (Serializable) data AmuletRules_AdvanceOpenMiningRoundsResult = AmuletRules_AdvanceOpenMiningRoundsResult with summarizingRoundCid : ContractId SummarizingMiningRound openRoundCid : ContractId OpenMiningRound + deriving (Serializable) data AmuletRules_MiningRound_StartIssuingResult = AmuletRules_MiningRound_StartIssuingResult with issuingRoundCid : ContractId IssuingMiningRound unclaimedDevelopmentFundCouponCid : Optional (ContractId UnclaimedDevelopmentFundCoupon) + deriving (Serializable) data AmuletRules_MiningRound_CloseResult = AmuletRules_MiningRound_CloseResult with closedRoundCid : ContractId ClosedMiningRound + deriving (Serializable) data AmuletRules_MiningRound_ArchiveResult = AmuletRules_MiningRound_ArchiveResult + deriving (Serializable) data AmuletRules_ClaimExpiredRewardsResult = AmuletRules_ClaimExpiredRewardsResult with unclaimedRewardCid : Optional (ContractId UnclaimedReward) + deriving (Serializable) -- The following results are intentionally empty to save on storage cost incurred in Scan. -- These choices are driven by automation, which reads its results indirectly via the update to their backing stores. data AmuletRules_ClaimExpiredRewardsV2Result = AmuletRules_ClaimExpiredRewardsV2Result {} + deriving (Serializable) data AmuletRules_StartProcessingRewardsV2Result = AmuletRules_StartProcessingRewardsV2Result {} + deriving (Serializable) data AmuletRules_UnhideRewardCouponsV2Result = AmuletRules_UnhideRewardCouponsV2Result {} + deriving (Serializable) data AmuletRules_ArchiveDryRunRewardAccountingV2Result = AmuletRules_ArchiveDryRunRewardAccountingV2Result {} + deriving (Serializable) data AmuletRules_MergeUnclaimedRewardsResult = AmuletRules_MergeUnclaimedRewardsResult with unclaimedRewardCid : ContractId UnclaimedReward + deriving (Serializable) data AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult = AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult with unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + deriving (Serializable) data AmuletRules_AllocateDevelopmentFundCouponResult = AmuletRules_AllocateDevelopmentFundCouponResult with developmentFundCouponCid : ContractId DevelopmentFundCoupon optUnclaimedDevelopmentFundCouponCid : Optional (ContractId UnclaimedDevelopmentFundCoupon) + deriving (Serializable) data AmuletRules_SetConfigResult = AmuletRules_SetConfigResult with newAmuletRules : ContractId AmuletRules + deriving (Serializable) data AmuletRules_AddFutureAmuletConfigScheduleResult = AmuletRules_AddFutureAmuletConfigScheduleResult with newAmuletRules : ContractId AmuletRules + deriving (Serializable) data AmuletRules_RemoveFutureAmuletConfigScheduleResult = AmuletRules_RemoveFutureAmuletConfigScheduleResult with newAmuletRules : ContractId AmuletRules + deriving (Serializable) data AmuletRules_UpdateFutureAmuletConfigScheduleResult = AmuletRules_UpdateFutureAmuletConfigScheduleResult with newAmuletRules : ContractId AmuletRules + deriving (Serializable) data AmuletRules_ConvertFeaturedAppActivityMarkersResult = AmuletRules_ConvertFeaturedAppActivityMarkersResult with appRewardCouponCids : [ContractId AppRewardCoupon] + deriving (Serializable) data AmuletRules_UpdateExternalPartyConfigStatesResult = AmuletRules_UpdateExternalPartyConfigStatesResult with newExternalPartyConfigStateCid : ContractId ExternalPartyConfigState - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The rules governing how Amulet users can modify the Amulet state managed by the DSO party. template AmuletRules @@ -189,10 +213,12 @@ template AmuletRules controller Set.toList (transferControllers transfer) do checkExpectedDso dso expectedDso + configUsd <- getValueAsOfLedgerTime configSchedule let rewardsConfig = RewardsIssuanceConfig with issueAppRewards = True issueValidatorRewards = True - executeTransfer (toInterfaceContractId self) rewardsConfig context dso transfer + executeTransfer (toInterfaceContractId self) rewardsConfig context dso + (getDevelopmentFundManagerBlacklist configUsd) transfer nonconsuming choice AmuletRules_CreateExternalPartySetupProposal : AmuletRules_CreateExternalPartySetupProposalResult -- ^ Propose to host an external party @@ -285,7 +311,8 @@ template AmuletRules Left failureStatus -> failWithStatus failureStatus Right _ -> pure () (trafficCostAmulet, trafficCostUsd) <- computeSynchronizerFees dso provider trafficAmount this context - (transferResult, meta) <- splitAndBurn (toInterfaceContractId self) provider trafficCostAmulet inputs context dso "traffic purchase" + (transferResult, meta) <- splitAndBurn (toInterfaceContractId self) provider trafficCostAmulet inputs context dso + (getDevelopmentFundManagerBlacklist configUsd) "traffic purchase" -- create a new MemberTraffic contract for the purchased traffic purchasedTraffic <- create MemberTraffic with dso @@ -823,6 +850,7 @@ template AmuletRules expiresAt : Time reason : Text fundManager : Party + mintAfter : Optional Time controller fundManager do -- Verify expiry @@ -836,6 +864,19 @@ template AmuletRules require ("controller is the configured Development Fund manager: " <> show developmentFundManager) $ developmentFundManager == fundManager + require ("fund manager is not blacklisted: " <> show fundManager) $ + fundManager `notElem` getDevelopmentFundManagerBlacklist configUsd + + -- sanity check that mintAfter is in the future + forA_ mintAfter $ assertWithinDeadline "mintAfter" + + whenSome configUsd.minDevelopmentFundMintingDelay $ \minDelay -> + case mintAfter of + None -> abort "DevelopmentFundCoupon cannot be allocated without a mintAfter time, as a minimum development fund minting delay is configured" + Some mintAfterTime -> + assertWithinDeadline "mintAfter - minDevelopmentFundMintingDelay" + (mintAfterTime `addRelTime` negate minDelay) + -- Verify the amount and create a leftover UnclaimedDevelopmentFundCoupon if needed totalUnclaimedDevelopmentFundCoupons <- sum <$> forA unclaimedDevelopmentFundCouponCids \cid -> do coupon <- fetchAndArchive (ForDso with dso) cid @@ -848,7 +889,7 @@ template AmuletRules -- record development fund coupon contract developmentFundCouponCid <- create DevelopmentFundCoupon with - dso; beneficiary; fundManager; amount; expiresAt; reason + dso; beneficiary; fundManager; amount; expiresAt; reason; mintAfter return AmuletRules_AllocateDevelopmentFundCouponResult with developmentFundCouponCid @@ -989,6 +1030,7 @@ template AmuletRules amuletPrice = validatedRounds.latestUsableRound.amuletPrice transferConfig = transferConfigToTransferConfigV2 validatedRounds.latestUsableRound.transferConfigUsd rewardCalculationVersion = (.mintingVersion) <$> validatedRounds.latestUsableRound.rewardConfig + amuletSwitchOverTimes = config.amuletSwitchOverTimes pure AmuletRules_UpdateExternalPartyConfigStatesResult with .. @@ -997,13 +1039,13 @@ data OpenMiningRoundTriple = OpenMiningRoundTriple round0Cid : ContractId OpenMiningRound round1Cid : ContractId OpenMiningRound round2Cid : ContractId OpenMiningRound - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data ValidatedOpenMiningRounds = ValidatedOpenMiningRounds with oldestRound : OpenMiningRound latestUsableRound : OpenMiningRound - deriving (Show, Eq) + deriving (Show, Eq, Serializable) validateOpenMiningRoundTriple : Party -> OpenMiningRoundTriple -> Update ValidatedOpenMiningRounds validateOpenMiningRoundTriple dso roundTriple = do @@ -1042,7 +1084,7 @@ data InvalidTransferReason | ExtInvalidTransferReason with dummyUnitField : () -- ^ Extension constructor (and field) to work around the current lack of upgrading for variants in Daml 3.0 - deriving (Eq, Show) + deriving (Eq, Show, Serializable) mkUnknownSynchronizerFailure : Text -> FailureStatus mkUnknownSynchronizerFailure synchronizerId = @@ -1115,12 +1157,13 @@ checkTransferConstraints transfer summary transferConfigAmulet data RewardsIssuanceConfig = RewardsIssuanceConfig with issueAppRewards : Bool issueValidatorRewards : Bool + deriving (Serializable) -- | Execute a transfer. -executeTransfer : ContractId TransferEventsV2.EventLog -> RewardsIssuanceConfig -> TransferContext -> Party -> Transfer -> Update TransferResult -executeTransfer eventLogCid config context dso t = do +executeTransfer : ContractId TransferEventsV2.EventLog -> RewardsIssuanceConfig -> TransferContext -> Party -> [Party] -> Transfer -> Update TransferResult +executeTransfer eventLogCid config context dso developmentFundManagerBlacklist t = do -- compute summaries - csum <- summarizeAndValidateContext eventLogCid context dso t + csum <- summarizeAndValidateContext eventLogCid context dso developmentFundManagerBlacklist t executeTransfer' config csum dso t -- | Execute a transfer for external parties so that the prepared tx remains valid for 24h @@ -1215,7 +1258,7 @@ data TransferContextSummary = TransferContextSummary with openRound : OpenMiningRound issuingMiningRounds : Map Round IssuingMiningRound validatorRights : Map Party (ContractId ValidatorRight) - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data TransferContextSummaryV2 = TransferContextSummaryV2 with dso : Party @@ -1227,7 +1270,8 @@ data TransferContextSummaryV2 = TransferContextSummaryV2 with validatorRights : Map Party (ContractId ValidatorRight) rewardCalculationVersion : Optional RewardVersion eventLogCid : Optional (ContractId TransferEventsV2.EventLog) - deriving (Eq, Show) + developmentFundManagerBlacklist : Optional [Party] + deriving (Eq, Show, Serializable) data TransferInputsSummary = TransferInputsSummary with totalAmuletAmount : Decimal @@ -1245,7 +1289,7 @@ data TransferInputsSummary = TransferInputsSummary with totalDevelopmentFundAmount : Optional Decimal -- ^ Note: Same rationale as above — made optional to ensure compatibility with -- the upgrade checker on package upload because `TransferInputsSummary` is serializable. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) type TransferOutputsSummary = [PreprocessedTransferOutput] @@ -1255,10 +1299,10 @@ data PreprocessedTransferOutput = PreprocessedTransferOutput with amount : Decimal -- ^ Amount of amulet held by this output (after deducting fees) lock : Optional TimeLock -- ^ Whether to lock the amulet or not meta : Optional Metadata -- ^ Metadata to attach to the output amulet - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -summarizeAndValidateContext : ContractId TransferEventsV2.EventLog -> TransferContext -> Party -> Transfer -> Update TransferContextSummaryV2 -summarizeAndValidateContext eventLogCid context dso tf = do +summarizeAndValidateContext : ContractId TransferEventsV2.EventLog -> TransferContext -> Party -> [Party] -> Transfer -> Update TransferContextSummaryV2 +summarizeAndValidateContext eventLogCid context dso developmentFundManagerBlacklist tf = do -- check that the current mining round is open openRound <- fetchReferenceData (ForDso with dso) context.openMiningRound assertDeadlineExceeded "openRound.opensAt" openRound.opensAt @@ -1293,6 +1337,7 @@ summarizeAndValidateContext eventLogCid context dso tf = do amuletPrice = openRound.amuletPrice rewardCalculationVersion = (.mintingVersion) <$> openRound.rewardConfig eventLogCid = Some eventLogCid + developmentFundManagerBlacklist = Some developmentFundManagerBlacklist summarizeAndValidateExternalPartyContext : ExternalPartyTransferContext -> Party -> Transfer -> Update TransferContextSummaryV2 @@ -1319,6 +1364,10 @@ summarizeAndValidateExternalPartyContext context dso tf = do amuletPrice = externalPartyConfigState.amuletPrice rewardCalculationVersion = externalPartyConfigState.rewardCalculationVersion eventLogCid = Some (toInterfaceContractId context.externalPartyConfigState) + -- External party context cannot be used with `DevelopmentFundCoupon` as of now. But to ensure + -- the blacklist is still enforced in the future once we add support, we set it to `None` here + -- to show that the list is unknown in the context. + developmentFundManagerBlacklist = None getValidatorRight : TransferContextSummaryV2 -> Party -> Update (ContractId ValidatorRight) getValidatorRight csum user = @@ -1467,7 +1516,14 @@ summarizeAndConsumeInputs csum dso sender inps = do summarizeAndConsumeInput _round s (InputDevelopmentFundCoupon couponCid) = do coupon <- fetchAndArchive forOwner couponCid + developmentFundManagerBlacklist <- case csum.developmentFundManagerBlacklist of + None -> abort "DevelopmentFundCoupons.developmentFundManagerBlacklist is not defined" + Some blacklist -> pure blacklist + require ("DevelopmentFundCoupon.fundManager is not blacklisted: " <> show coupon.fundManager) + (coupon.fundManager `notElem` developmentFundManagerBlacklist) assertWithinDeadline "DevelopmentFundCoupon.expiresAt" coupon.expiresAt + whenSome coupon.mintAfter $ \m -> do + assertDeadlineExceeded "DevelopmentFundCoupon.mintAfter" m return TransferInputsSummary with totalAmuletAmount = s.totalAmuletAmount totalAppRewardAmount = s.totalAppRewardAmount @@ -1741,7 +1797,7 @@ data AppTransferContext = AppTransferContext amuletRules : ContractId AmuletRules openMiningRound : ContractId OpenMiningRound featuredAppRight : Optional (ContractId FeaturedAppRight) - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- Adapter for choices that need to return an `AmuletCreateSummary` but call a choice internally that no longer returns one. legacyAmuletCreateSummary : Party -> Party -> ContractId OpenMiningRound -> ContractId Amulet -> Update (AmuletCreateSummary (ContractId Amulet)) @@ -1778,7 +1834,7 @@ data PaymentTransferContext = PaymentTransferContext with amuletRules : ContractId AmuletRules context : TransferContext - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Contracts that need to be passed in to a Transfer so that @@ -1789,7 +1845,7 @@ data TransferContext = TransferContext issuingMiningRounds : Map Round (ContractId IssuingMiningRound) validatorRights : Map Party (ContractId ValidatorRight) -- ^ Map from user to ValidatorRight contract. featuredAppRight : Optional (ContractId FeaturedAppRight) -- ^ Optional proof that the provider is a featured app provider. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Contracts that need to be passed in to a Transfer so that -- we can reference them by contract id instead of by key. @@ -1799,7 +1855,7 @@ data ExternalPartyTransferContext = ExternalPartyTransferContext -- no issuing mining round and validator rights, those are -- required for minting rewards which must go through the AmuletRules_Transfer choice. featuredAppRight : Optional (ContractId FeaturedAppRight) -- ^ Optional proof that the provider is a featured app provider. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Helper to construct transfer context with only amulet inputs @@ -1816,7 +1872,7 @@ data CreatedAmulet | ExtCreatedAmulet with dummyUnitField : () -- ^ Extension constructor (and field) to work around the current lack of upgrading for variants in Daml 3.0 - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) createdAmuletToHolding : CreatedAmulet -> ContractId Api.Token.HoldingV1.Holding createdAmuletToHolding = \case @@ -1839,7 +1895,7 @@ data TransferInput | InputUnclaimedActivityRecord (ContractId UnclaimedActivityRecord) | InputDevelopmentFundCoupon (ContractId DevelopmentFundCoupon) | InputRewardCouponV2 (ContractId RewardCouponV2) - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | Smart constructor for inputing validator faucet coupons into a transfer. mkInputValidatorFaucetCoupon : ContractId ValidatorFaucetCoupon -> TransferInput @@ -1853,7 +1909,7 @@ data Transfer = Transfer with outputs : [TransferOutput] beneficiaries : Optional [AppRewardBeneficiary] -- ^ Beneficiaries between which the app reward is split. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | An individual output for a batch transfer. data TransferOutput = TransferOutput with @@ -1870,7 +1926,7 @@ data TransferOutput = TransferOutput with -- ^ The lock to be added, if any. meta : Optional Metadata -- ^ Token standard metadata to include in the transfer event log. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) data TransferResult = TransferResult @@ -1882,7 +1938,7 @@ data TransferResult -- ^ Optional reference to the amulet for the change returned to the sender. -- Only created if there was some change to be returned after deducting the fee for returning change. meta : Optional Api.Token.MetadataV1.Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data AmuletRules_BuyMemberTrafficResult = AmuletRules_BuyMemberTrafficResult with round : Round @@ -1891,7 +1947,7 @@ data AmuletRules_BuyMemberTrafficResult = AmuletRules_BuyMemberTrafficResult wit purchasedTraffic : ContractId MemberTraffic senderChangeAmulet : Optional (ContractId Amulet) meta : Optional Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data AmuletRules_CreateExternalPartySetupProposalResult = AmuletRules_CreateExternalPartySetupProposalResult with @@ -1901,7 +1957,7 @@ data AmuletRules_CreateExternalPartySetupProposalResult = AmuletRules_CreateExte transferResult : TransferResult amuletPaid : Decimal meta : Optional Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data AmuletRules_CreateTransferPreapprovalResult = AmuletRules_CreateTransferPreapprovalResult with @@ -1909,7 +1965,7 @@ data AmuletRules_CreateTransferPreapprovalResult = AmuletRules_CreateTransferPre transferResult : TransferResult amuletPaid : Decimal meta : Optional Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Summary of input and output amounts and fees paid. -- This summary is intended to be used together with the `Transfer` specification @@ -1947,7 +2003,7 @@ data TransferSummary = TransferSummary with inputDevelopmentFundAmount : Optional Decimal -- ^ Total amount of development fund coupon issuance input into this transfer. -- Note: Made optional as the addition of this field is checked by the upgrade checker. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data BalanceChange = BalanceChange with changeToInitialAmountAsOfRoundZero : Decimal @@ -1955,7 +2011,7 @@ data BalanceChange = BalanceChange with -- a amulet created in round 3 is treated as a amulet created in round 0 with a higher initial amount. changeToHoldingFeesRate : Decimal -- ^ The change of total holding fees introduced by this balance change. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) instance Additive BalanceChange where (+) l r = BalanceChange with @@ -2019,17 +2075,17 @@ data ExternalPartySetupProposal_AcceptResult = ExternalPartySetupProposal_Accept with validatorRightCid : ContractId ValidatorRight transferPreapprovalCid : ContractId TransferPreapproval - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data ExternalPartySetupProposal_RejectResult = ExternalPartySetupProposal_RejectResult with dummyArg : () -- to ensure this can be upgraded as a record - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data ExternalPartySetupProposal_WithdrawResult = ExternalPartySetupProposal_WithdrawResult with dummyArg : () -- to ensure this can be upgraded as a record - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- Transfer Pre-approval @@ -2186,13 +2242,13 @@ data TransferPreapproval_SendResult = TransferPreapproval_SendResult with result : TransferResult meta : Optional Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferPreapproval_SendV2Result = TransferPreapproval_SendV2Result with result : TransferResult meta : Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferPreapproval_RenewResult = TransferPreapproval_RenewResult with @@ -2202,13 +2258,13 @@ data TransferPreapproval_RenewResult = TransferPreapproval_RenewResult provider: Party amuletPaid : Decimal meta : Optional Metadata - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferPreapproval_ExpireResult = TransferPreapproval_ExpireResult {} - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferPreapproval_CancelResult = TransferPreapproval_CancelResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) computeTransferPreapprovalFee : RelTime -> AmuletConfig Unit.USD -> TransferContext -> Party -> Update (Decimal, Decimal) computeTransferPreapprovalFee duration amuletConfig context dso = do @@ -2218,13 +2274,13 @@ computeTransferPreapprovalFee duration amuletConfig context dso = do let feeAmulet = feeUsd / contextMiningRound.amuletPrice pure (feeAmulet, feeUsd) -splitAndBurn : ContractId TransferEventsV2.EventLog -> Party -> Decimal -> [TransferInput] -> TransferContext -> Party -> Text -> Update (TransferResult, Metadata) -splitAndBurn eventLogCid sender amount inputs context dso usage = do +splitAndBurn : ContractId TransferEventsV2.EventLog -> Party -> Decimal -> [TransferInput] -> TransferContext -> Party -> [Party] -> Text -> Update (TransferResult, Metadata) +splitAndBurn eventLogCid sender amount inputs context dso developmentFundManagerBlacklist usage = do -- do not create activity records as part of the transfer. We create a validator activity record for the full burn below. let rewardsConfig = RewardsIssuanceConfig with issueAppRewards = False issueValidatorRewards = False - transferResult <- executeTransfer eventLogCid rewardsConfig context dso Transfer with + transferResult <- executeTransfer eventLogCid rewardsConfig context dso developmentFundManagerBlacklist Transfer with sender provider = sender inputs @@ -2302,7 +2358,8 @@ payForTransferPreapproval reason dso provider context inputs expiresAt newExpire , emptyMetadata) else do (amuletPaid, _) <- computeTransferPreapprovalFee paidExtension configUsd context.context dso - (transferResult, meta) <- splitAndBurn (toInterfaceContractId context.amuletRules) provider amuletPaid inputs context.context dso reason + (transferResult, meta) <- splitAndBurn (toInterfaceContractId context.amuletRules) provider amuletPaid inputs context.context dso + (getDevelopmentFundManagerBlacklist configUsd) reason pure (amuletPaid, transferResult, meta) -- Token standard context parsing @@ -2342,6 +2399,7 @@ bootstrapExternalPartyConfigState AmuletRules{..} openMiningRoundTriple = do amuletPrice = validatedRounds.latestUsableRound.amuletPrice transferConfig = transferConfig rewardCalculationVersion = (.mintingVersion) <$> validatedRounds.latestUsableRound.rewardConfig + amuletSwitchOverTimes = config.amuletSwitchOverTimes configState1 = configState0 with targetArchiveAfter = configState0.targetArchiveAfter `addRelTime` getExternalPartyConfigStateTickDuration config create configState0 diff --git a/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml b/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml index a26bbf5686f..bed669355d9 100644 --- a/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml +++ b/daml/splice-amulet/daml/Splice/DecentralizedSynchronizer.daml @@ -23,14 +23,14 @@ data AmuletDecentralizedSynchronizerConfig = AmuletDecentralizedSynchronizerConf activeSynchronizer : Text -- ^ The synchronizer-id of the synchronizer on which the SVs accepts transactions involving the -- AmuletRules and related contracts, which must be one of the `requiredSynchronizers`. fees : SynchronizerFeesConfig -- ^ The fees charged for using the decentralized synchronizer. We use the same fees across all active decentralized synchronizers. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The limits defining the free base rate delivered by the synchronizer. -- It defines the base rate as allowing at most burstAmount of sequencing traffic within a window of burstWindow seconds. data BaseRateTrafficLimits = BaseRateTrafficLimits with burstAmount: Int -- ^ The total burstAmount in bytes of sequencing traffic delivered over the burstWindow burstWindow: RelTime -- ^ Time window within which the burstAmount must not be exceeded - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Synchronizer fees related configuration to be tracked on the DsoRules contract data SynchronizerFeesConfig = SynchronizerFeesConfig with @@ -43,7 +43,7 @@ data SynchronizerFeesConfig = SynchronizerFeesConfig with -- smaller one than the cost of a write. minTopupAmount: Int -- ^ The minimum amount of extra traffic (in bytes) that must be bought when buying extra traffic. -- This ensures that the SVs can amortize the cost of executing that transaction. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) validAmuletDecentralizedSynchronizerConfig : AmuletDecentralizedSynchronizerConfig -> Bool @@ -92,7 +92,7 @@ data ForMemberTraffic = ForMemberTraffic with memberId : Text synchronizerId : Text migrationId: Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance HasCheckedFetch MemberTraffic ForDso where contractGroupId MemberTraffic{..} = ForDso with .. diff --git a/daml/splice-amulet/daml/Splice/Expiry.daml b/daml/splice-amulet/daml/Splice/Expiry.daml index e9c72e6badd..9b3a3d04e5b 100644 --- a/daml/splice-amulet/daml/Splice/Expiry.daml +++ b/daml/splice-amulet/daml/Splice/Expiry.daml @@ -35,7 +35,7 @@ data BoundedSet a = Singleton a -- ^ `Singleton x` represents the singleton set `{x}` | AfterMaxBound -- ^ `AfterMaxBound` represents the set of all values larger than the maximal value that can be represented by type `a` - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Note: be careful when using this instance: `fmap (- 100) AfterMaxBound` will return -- `AfterMaxBound`, but should actually return an error. @@ -135,7 +135,7 @@ data TimeLock = TimeLock with -- Intended for wallets to inform the user about the reason for the lock. -- Consider what details to share about the context, as that might be private and -- the string here will be public information. - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) -- | `isAmuletExpired openRound amuletAmount` computes whether the expiring `amuletAmount` -- is definitely expired in case the `openRound` is open. diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml index a6c281255e8..ae95ae8d76b 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -36,16 +36,16 @@ data ExternalPartyAmuletRules_ExpireAmuletAllocationInput = ExternalPartyAmuletR with allocationCid : ContractId AmuletAllocation expireLock : Bool - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data ExternalPartyAmuletRules_ExpireAmuletAllocationsResult = ExternalPartyAmuletRules_ExpireAmuletAllocationsResult with results : [AmuletAllocation_DsoExpireResult] - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data ExternalPartyAmuletRules_ExpireAmuletAllocationsV2Result = ExternalPartyAmuletRules_ExpireAmuletAllocationsV2Result {} -- intentionally no results to save storage space in Scan - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Rules contract that can be used in transactions that require signatures -- from an external party. This is intended to get archived and recreated as rarely as possible @@ -197,7 +197,7 @@ template ExternalPartyAmuletRules data ExternalPartyAmuletRules_CreateTransferCommandResult = ExternalPartyAmuletRules_CreateTransferCommandResult with transferCommandCid : ContractId TransferCommand - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | A contract tracking the number of completed TransferCommands per sender, -- which is used to determine the nonces used in TransferCommands for deduplication. @@ -313,26 +313,26 @@ data TransferCommand_SendResult = TransferCommand_SendResult result : TransferCommandResult sender : Party -- Returned to simplify tx log parsing nonce : Int -- Returned to simplify tx log parsing - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferCommandResult = TransferCommandResultFailure with reason : InvalidTransferReason | TransferCommandResultSuccess with result : TransferResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferCommand_WithdrawResult = TransferCommand_WithdrawResult with sender : Party -- Returned to simplify tx log parsing nonce : Int -- Returned to simplify tx log parsing - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TransferCommand_ExpireResult = TransferCommand_ExpireResult with sender : Party -- Returned to simplify tx log parsing nonce : Int -- Returned to simplify tx log parsing - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- Token standard transfer factory diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml b/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml index 9adee1ea9a6..c9115e9f3a7 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml @@ -3,6 +3,8 @@ module Splice.ExternalPartyConfigState where +import DA.TextMap + import Splice.Api.Token.MetadataV1 import Splice.Api.Token.TransferEventsV2 as TransferEventsV2 @@ -27,6 +29,7 @@ template ExternalPartyConfigState transferConfig : TransferConfigV2 Unit.USD -- ^ Transfer config at the time the config state was created. targetArchiveAfter : Time -- ^ Lower bound on the time the contract gets archived, not enforced as a strict upper bound. rewardCalculationVersion : Optional RewardVersion -- ^ The reward calculation version to use for transactions relying on this config state. + amuletSwitchOverTimes : Optional (TextMap Time) -- ^ Timepoints for synchronized switch-overs of amulet functionality both on and off-ledger. where signatory dso diff --git a/daml/splice-amulet/daml/Splice/Fees.daml b/daml/splice-amulet/daml/Splice/Fees.daml index 2f533d05892..fed7886db9a 100644 --- a/daml/splice-amulet/daml/Splice/Fees.daml +++ b/daml/splice-amulet/daml/Splice/Fees.daml @@ -32,7 +32,7 @@ relTimeToSeconds dt = relTimeToMicros dt / 1e6 ------------------------- newtype RatePerRound = RatePerRound with rate : Decimal - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) chargeRatePerRound : RatePerRound -> RelRound -> Decimal chargeRatePerRound rate dt = rate.rate * intToDecimal dt.diff @@ -53,7 +53,7 @@ positiveRatePerRound r = r.rate >= 0.0 -- | A fixed fee independent of the action being taken. -- TODO(M3-90): check whether this name matches usage in financial terms, it probably isn't. Should it be 'flat-fee', 'constantfee', ... ??? newtype FixedFee = FixedFee with fee : Decimal - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) scaleFixedFee : Decimal -> FixedFee -> FixedFee scaleFixedFee s f = FixedFee with fee = s * f.fee @@ -69,7 +69,7 @@ data SteppedRate = SteppedRate with initialRate : Decimal steps : [(Decimal, Decimal)] - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) validSteppedRate : SteppedRate -> Bool validSteppedRate SteppedRate{initialRate, steps} = @@ -124,7 +124,7 @@ data ExpiringAmount = ExpiringAmount with initialAmount : Decimal createdAt : Round ratePerRound : RatePerRound - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) validExpiringAmount : ExpiringAmount -> Bool validExpiringAmount e = @@ -160,7 +160,7 @@ instance Patchable RatePerRound where ---------------- data RatePerDay = RatePerDay with rate : Decimal - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) chargeRatePerDay : RatePerDay -> RelTime -> Decimal chargeRatePerDay rate dt = rate.rate * relTimeToDays dt diff --git a/daml/splice-amulet/daml/Splice/Issuance.daml b/daml/splice-amulet/daml/Splice/Issuance.daml index 51f01798b6a..90b2d3e4776 100644 --- a/daml/splice-amulet/daml/Splice/Issuance.daml +++ b/daml/splice-amulet/daml/Splice/Issuance.daml @@ -27,7 +27,7 @@ data IssuanceConfig = IssuanceConfig with -- Introduced as part of CIP-0003. Will default to 0.00 USD once CIP-0096 becomes effective. optDevelopmentFundPercentage : Optional Decimal -- ^ Percentage of each mint emission allocated to the Development Fund under CIP-0082. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Getter with the right default value for the validator faucet cap. -- Use this consistently instead of accessing the field directly. @@ -61,7 +61,7 @@ data OpenMiningRoundSummary = OpenMiningRoundSummary with totalSvRewardWeight : Int optTotalValidatorFaucetCoupons : Optional Int -- ^ Introduced as part of CIP-3. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) getTotalValidatorFaucetCoupons : OpenMiningRoundSummary -> Int getTotalValidatorFaucetCoupons = fromOptional 0 . (.optTotalValidatorFaucetCoupons) @@ -79,7 +79,7 @@ data IssuingRoundParameters = IssuingRoundParameters with optAmuletsToIssueToDevelopmentFund : Optional Decimal -- ^ Note: Made optional as the addition of this field is checked by the upgrade checker -- on package upload because `IssuingRoundParameters` is serializable. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) validateOpenMiningRoundSummary : CanAssert m => OpenMiningRoundSummary -> m () validateOpenMiningRoundSummary summary = do @@ -156,6 +156,7 @@ data IssuanceTranche = IssuanceTranche with rewardsToIssue : Decimal -- ^ Total amulets to issue as rewards in this tranche issuancePerCoupon : Decimal -- ^ Issuence per reward coupon for this tranche unclaimedRewards : Decimal -- ^ Amulets to issue in this tranche that were not claimed + deriving (Serializable) -- | `computeIssuanceTranche rewardsToIssue capPerCoupon totalCoupons` -- diff --git a/daml/splice-amulet/daml/Splice/RelRound.daml b/daml/splice-amulet/daml/Splice/RelRound.daml index 497fe4f5b1d..9f763e9f5d7 100644 --- a/daml/splice-amulet/daml/Splice/RelRound.daml +++ b/daml/splice-amulet/daml/Splice/RelRound.daml @@ -11,7 +11,7 @@ import Splice.Types (Round(..)) -- | The equivalent of RelTime for rounds, i.e., a delta between -- two rounds newtype RelRound = RelRound with diff : Int - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) addRelRound : Round -> RelRound -> Round addRelRound round i = Round with number = round.number + i.diff diff --git a/daml/splice-amulet/daml/Splice/Schedule.daml b/daml/splice-amulet/daml/Splice/Schedule.daml index 96e384fbe47..fd0a2f63ca3 100644 --- a/daml/splice-amulet/daml/Splice/Schedule.daml +++ b/daml/splice-amulet/daml/Splice/Schedule.daml @@ -16,7 +16,7 @@ import DA.Time data Schedule t a = Schedule with initialValue : a -- ^ Initial value to use until the future values come into effect. futureValues : [(t, a)] -- ^ sorted in ascending order - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance Functor (Schedule t) where fmap f (Schedule{..}) = Schedule with diff --git a/daml/splice-amulet/daml/Splice/Types.daml b/daml/splice-amulet/daml/Splice/Types.daml index a46b44d1cda..0a792ea4350 100644 --- a/daml/splice-amulet/daml/Splice/Types.daml +++ b/daml/splice-amulet/daml/Splice/Types.daml @@ -16,7 +16,7 @@ import Splice.Util (HasCheckedFetch(..)) import Splice.TokenStandard.Utils (accountPrincipal) newtype Round = Round with number : Int - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) -- | Check if a round has a positive round number isDefinedRound : Round -> Bool @@ -28,7 +28,7 @@ isDefinedRound round = round.number >= 0 -- Used for checked fetches. data ForDso = ForDso with dso : Party - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The group of contracts managed by the DSO party for a specific mining round. @@ -37,7 +37,7 @@ data ForDso = ForDso with data ForRound = ForRound with dso : Party round : Round - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The group of contracts managed by the DSO party and owned by a specific party. -- @@ -45,7 +45,7 @@ data ForRound = ForRound with data ForOwner = ForOwner with dso : Party owner : Party - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance HasCheckedFetch V1.HoldingView ForOwner where diff --git a/daml/splice-amulet/daml/Splice/ValidatorLicense.daml b/daml/splice-amulet/daml/Splice/ValidatorLicense.daml index 1c7de6f285d..f02d41519b9 100644 --- a/daml/splice-amulet/daml/Splice/ValidatorLicense.daml +++ b/daml/splice-amulet/daml/Splice/ValidatorLicense.daml @@ -16,29 +16,37 @@ data FaucetState = FaucetState with firstReceivedFor : Round -- ^ The first round for which a coupon was received. lastReceivedFor : Round -- ^ The last round for which a coupon was received. numCouponsMissed : Int -- ^ The number of rounds for which no coupon was received. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data ValidatorLicense_ReceiveFaucetCouponResult = ValidatorLicense_ReceiveFaucetCouponResult with licenseCid : ContractId ValidatorLicense couponCid : ContractId ValidatorFaucetCoupon + deriving (Serializable) data ValidatorLicense_RecordValidatorLivenessActivityResult = ValidatorLicense_RecordValidatorLivenessActivityResult with licenseCid : ContractId ValidatorLicense couponCid : ContractId ValidatorLivenessActivityRecord + deriving (Serializable) data ValidatorLicense_WithdrawResult = ValidatorLicense_WithdrawResult + deriving (Serializable) data ValidatorLicense_CancelResult = ValidatorLicense_CancelResult + deriving (Serializable) data ValidatorLicense_UpdateMetadataResult = ValidatorLicense_UpdateMetadataResult with - licenseCid : ContractId ValidatorLicense + licenseCid : ContractId ValidatorLicense + deriving (Serializable) data ValidatorLicense_ReportActiveResult = ValidatorLicense_ReportActiveResult with - licenseCid : ContractId ValidatorLicense + licenseCid : ContractId ValidatorLicense + deriving (Serializable) data ValidatorFaucetCoupon_DsoExpireResult = ValidatorFaucetCoupon_DsoExpireResult + deriving (Serializable) data ValidatorLivenessActivityRecord_DsoExpireResult = ValidatorLivenessActivityRecord_DsoExpireResult + deriving (Serializable) data ValidatorLicenseMetadata = ValidatorLicenseMetadata with @@ -46,7 +54,7 @@ data ValidatorLicenseMetadata = ValidatorLicenseMetadata version : Text -- ^ The version the validator is currently on contactPoint : Text -- ^ A contact point that can be used to reach the operator of the validator in case there are issues with the validator. -- This can be an email address or a slack user name. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The existence of a validator license is what makes a validator an (onboarded) validator. template ValidatorLicense with diff --git a/daml/splice-api-reward-assignment-v1/daml.yaml b/daml/splice-api-reward-assignment-v1/daml.yaml index f1943f43895..fe2afa307bc 100644 --- a/daml/splice-api-reward-assignment-v1/daml.yaml +++ b/daml/splice-api-reward-assignment-v1/daml.yaml @@ -11,6 +11,7 @@ dependencies: data-dependencies: - ../dars/splice-api-token-metadata-v1-1.0.0.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml b/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml index aeb34fefb4d..a508d6122a3 100644 --- a/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml +++ b/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml @@ -25,7 +25,7 @@ data RewardCouponView = RewardCouponView -- ^ The maximum number of new beneficiaries that can be assigned to the coupon in a single assignment. meta : Metadata -- ^ Metadata associated with this coupon. Provided for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Specification of a beneficiary of rewards. data RewardBeneficiary = RewardBeneficiary @@ -34,7 +34,7 @@ data RewardBeneficiary = RewardBeneficiary -- ^ The party that is granted the right to mint amulet for this activity. percentage : Decimal -- ^ Percentage (specified as a number between 0.0 and 1.0) of the reward minting allowance to assign to this beneficiary . - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) -- | A coupon representing the right to mint a certain amount of rewards. interface RewardCoupon where @@ -73,4 +73,4 @@ interface RewardCoupon where data RewardCoupon_AssignBeneficiariesResult = RewardCoupon_AssignBeneficiariesResult with newBeneficiariesCouponCids : [(Party, [ContractId RewardCoupon])] -- ^ The coupons created for the newly assigned beneficiaries. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) diff --git a/daml/splice-api-token-burn-mint-v1/daml.yaml b/daml/splice-api-token-burn-mint-v1/daml.yaml index 41404295993..36c69386506 100644 --- a/daml/splice-api-token-burn-mint-v1/daml.yaml +++ b/daml/splice-api-token-burn-mint-v1/daml.yaml @@ -12,6 +12,7 @@ data-dependencies: - ../dars/splice-api-token-holding-v1-1.0.0.dar - ../dars/splice-api-token-metadata-v1-1.0.0.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/daml/splice-api-token-burn-mint-v1/daml/Splice/Api/Token/BurnMintV1.daml b/daml/splice-api-token-burn-mint-v1/daml/Splice/Api/Token/BurnMintV1.daml index 2596ae2c88a..798497e07be 100644 --- a/daml/splice-api-token-burn-mint-v1/daml/Splice/Api/Token/BurnMintV1.daml +++ b/daml/splice-api-token-burn-mint-v1/daml/Splice/Api/Token/BurnMintV1.daml @@ -80,7 +80,7 @@ data BurnMintOutput = BurnMintOutput context : ChoiceContext -- ^ Context specific to this output which can be used to support locking -- or other registry-specific features. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The result of calling the `BurnMintFactory_BurnMint` choice. data BurnMintFactory_BurnMintResult = BurnMintFactory_BurnMintResult @@ -88,7 +88,7 @@ data BurnMintFactory_BurnMintResult = BurnMintFactory_BurnMintResult outputCids : [ContractId Holding] -- ^ The holdings created by the choice. -- Must contain exactly the outputs specified in the choice argument in the same order. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The view of a `BurnMintFactory`. data BurnMintFactoryView = BurnMintFactoryView @@ -98,4 +98,4 @@ data BurnMintFactoryView = BurnMintFactoryView -- for which this burnt-mint factory can be used. meta : Metadata -- ^ Additional metadata specific to the burn-mint factory, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) diff --git a/daml/splice-dso-governance-test/daml.yaml b/daml/splice-dso-governance-test/daml.yaml index f36097b9076..e9b1a690179 100644 --- a/daml/splice-dso-governance-test/daml.yaml +++ b/daml/splice-dso-governance-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-dso-governance-test source: daml -version: 0.1.34 +version: 0.1.35 dependencies: - daml-prim - daml-stdlib @@ -30,6 +30,7 @@ data-dependencies: - ../../token-standard/splice-token-standard-v1-test/.daml/dist/splice-token-standard-v1-test-current.dar - ../../token-standard/splice-token-standard-v2-test/.daml/dist/splice-token-standard-v2-test-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml index fefc74eef10..96c24111be8 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml @@ -88,6 +88,7 @@ initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound amuletPrice o nextScheduledSynchronizerUpgrade = None voteCooldownTime = None -- use default value of 1 minute nextScheduledLogicalSynchronizerUpgrade = None + svOperationsSwitchOverTimes = None let amuletConfig = fromOptional defaultAmuletConfig optAmuletConfig let ansRulesConfig = defaultAnsRulesConfig diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml index cd336915346..90a3dca02d5 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml @@ -8,6 +8,7 @@ import DA.Assert import DA.Foldable (forA_) import DA.List (dedupSort) import qualified DA.Map as Map +import DA.TextMap qualified as TextMap import DA.Optional (fromSomeNote) import Daml.Script import DA.Time @@ -143,7 +144,7 @@ testDevelopmentFundCouponExpiry = do reason = "Alice fixed issue XXX" alice <- setupUser app "alice" app.dso developmentFundCouponCid <- (.developmentFundCouponCid) <$> - allocateDevelopmentFundCoupon app fundManager alice.primaryParty amount expiresAt reason [unclaimedDevelopmentFundCouponCid] + allocateDevelopmentFundCoupon app fundManager alice.primaryParty amount expiresAt reason [unclaimedDevelopmentFundCouponCid] None -- Unhappy - expiresAt has not been reached submitMustFail (actAs sv1 <> readAs app.dso) do @@ -530,7 +531,11 @@ testTransferPreapprovalExpiry = do testBootstrapExternalPartyConfigState : Script () testBootstrapExternalPartyConfigState = do - (_, dso, (sv1, sv2, sv3, _)) <- initDevNet + (app, dso, (sv1, sv2, sv3, _)) <- initDevNet + -- set switch-over times so we can check they propagate into the bootstrapped config state + now <- getTime + let switchOverTimes = TextMap.fromList [("a", now), ("b", addRelTime now (days 1))] + updateAmuletConfig app (\c -> c with amuletSwitchOverTimes = Some switchOverTimes) -- archive to simulate bootstrapping with an older version configStates <- query @ExternalPartyConfigState dso submit dso $ forA configStates $ \(cid, _) -> archiveCmd cid @@ -566,6 +571,7 @@ testBootstrapExternalPartyConfigState = do configStates <- query @ExternalPartyConfigState dso length configStates === 2 + forA_ configStates $ \(_, cs) -> cs.amuletSwitchOverTimes === Some switchOverTimes pure () diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml index ca17a103b9b..aec758b39b6 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml @@ -10,6 +10,7 @@ import qualified DA.Map as Map import qualified DA.Set as Set import Daml.Script import DA.Time +import DA.TextMap qualified as TextMap import Splice.Amulet import Splice.AmuletRules @@ -613,6 +614,7 @@ testDsoRulesConfigChange = do nextScheduledSynchronizerUpgrade = Some (SynchronizerUpgradeSchedule with time = addRelTime now (days 4) migrationId = 1) + svOperationsSwitchOverTimes = Some (TextMap.fromList [("a", now), ("b", addRelTime now (days 1))]) let initialSynchronizerConfig2 = SynchronizerConfig with @@ -803,19 +805,20 @@ testAmuletRulesConfigChange = do [ ] + now <- getTime + -- first config that changes half of the parameters let newConfig1 = amuletRules.configSchedule.initialValue with transferConfig = defaultTransferConfig2 issuanceCurve = defaultIssuanceCurve2 tickDuration = seconds 200 optDevelopmentFundManager = Some fundManager + amuletSwitchOverTimes = Some (TextMap.fromList [("a", now), ("b", addRelTime now (days 1))]) -- second config that changes the other half of the parameters let newConfig2 = amuletRules.configSchedule.initialValue with decentralizedSynchronizer = defaultAmuletDecentralizedSynchronizerConfig2 - now <- getTime - -- creation of two concurrent independent vote requests [(amuletRulesCid, amuletRules)] <- query @AmuletRules dso result1 <- submit (actAs sv1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_RequestVote with diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestPatching.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestPatching.daml index 5ffaff51310..7a8ffa66b0e 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestPatching.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestPatching.daml @@ -12,6 +12,7 @@ import DA.Set() import DA.Set qualified as Set import DA.Map() import qualified DA.Map as Map +import DA.TextMap qualified as TextMap import Splice.Util @@ -22,7 +23,7 @@ import Splice.Util data RecordValue = RecordValue with x : Int y : Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance Patchable RecordValue where patch (RecordValue x1 y1) (RecordValue x2 y2) (RecordValue x3 y3) = RecordValue (patch x1 x2 x3) (patch y1 y2 y3) @@ -82,6 +83,23 @@ testPatchMap = script do test new base current = do patch (Map.fromList [("a", new)]) (Map.fromList [("a", base)]) (Map.fromList [("a", current), ("b", base)]) === (Map.fromList [("a", patch new base current), ("b", base)]) +testPatchTextMap : Script () +testPatchTextMap = script do + -- no change applied + patch (TextMap.fromList [("a", 1)]) (TextMap.fromList [("a", 1)]) (TextMap.fromList [("a", 1), ("b", 2)]) === (TextMap.fromList [("a", 1), ("b", 2)]) + -- addition of new (key, value) + patch (TextMap.fromList [("a", 1), ("c", 3)]) (TextMap.fromList [("a", 1)]) (TextMap.fromList [("a", 1), ("b", 2)]) === (TextMap.fromList [("a", 1), ("b", 2), ("c", 3)]) + -- removal of existing (key, value) + patch (TextMap.fromList []) (TextMap.fromList [("a", 1)]) (TextMap.fromList [("a", 1), ("b", 2)]) === (TextMap.fromList [("b", 2)]) + -- change of value for existing key + test 1 10 2 + test "a" "b" "c" + test (TextMap.fromList [("a", 1), ("b", 2)]) (TextMap.fromList [("b", 2), ("c", 3)]) (TextMap.fromList [("b", 2), ("d", 4)]) + test (RecordValue 2 2) (RecordValue 2 3) (RecordValue 5 4) + where + test new base current = do + patch (TextMap.fromList [("a", new)]) (TextMap.fromList [("a", base)]) (TextMap.fromList [("a", current), ("b", base)]) === (TextMap.fromList [("a", patch new base current), ("b", base)]) + testPatchOptional : Script () testPatchOptional = script do test 1 2 3 diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestUnallocatedUnclaimedActivityRecord.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestUnallocatedUnclaimedActivityRecord.daml index beaae5368fe..44261adc452 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestUnallocatedUnclaimedActivityRecord.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestUnallocatedUnclaimedActivityRecord.daml @@ -255,6 +255,7 @@ data TestContext = TestContext with amuletRulesCid : ContractId AmuletRules amountToMint : Decimal unclaimedRewardAmounts : [Decimal] + deriving (Serializable) allocateMustFail : ContractId UnallocatedUnclaimedActivityRecord -> TestContext -> Script () allocateMustFail unallocatedUnclaimedActivityRecordCid TestContext{..} = do diff --git a/daml/splice-dso-governance/daml.yaml b/daml/splice-dso-governance/daml.yaml index 753e6942c7f..6b9496ba6ef 100644 --- a/daml/splice-dso-governance/daml.yaml +++ b/daml/splice-dso-governance/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-dso-governance source: daml -version: 0.1.28 +version: 0.1.29 dependencies: - daml-prim - daml-stdlib @@ -12,6 +12,7 @@ data-dependencies: - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar - ../../token-standard/splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - -Wno-deprecated-exceptions diff --git a/daml/splice-dso-governance/daml/Splice/CometBft.daml b/daml/splice-dso-governance/daml/Splice/CometBft.daml index b6bf2247a02..d2441f70738 100644 --- a/daml/splice-dso-governance/daml/Splice/CometBft.daml +++ b/daml/splice-dso-governance/daml/Splice/CometBft.daml @@ -39,17 +39,17 @@ General approach for CometBFT integration: data CometBftNodeConfig = CometBftNodeConfig with validatorPubKey : Text votingPower : Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Config for a key used by the SvApp to create CometBFT network governance transactions. data GovernanceKeyConfig = GovernanceKeyConfig with pubKey : Text - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) -- | Config for a key used by the CometBFT Sequencer Driver to sequence messages via the CometBFT network. data SequencingKeyConfig = SequencingKeyConfig with pubKey : Text - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) -- | Config for all CometBFT nodes and keys under the control of a single SV node operator. data CometBftConfig = CometBftConfig with @@ -57,7 +57,7 @@ data CometBftConfig = CometBftConfig with -- ^ A map from CometBft node-ids to their configuration. governanceKeys : [GovernanceKeyConfig] sequencingKeys : [SequencingKeyConfig] - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Limits on the configurations that SV node operators can choose for their CometBFT nodes and keys. data CometBftConfigLimits = CometBftConfigLimits with @@ -66,7 +66,7 @@ data CometBftConfigLimits = CometBftConfigLimits with maxNumSequencingKeys : Int maxNodeIdLength : Int maxPubKeyLength : Int - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- default values diff --git a/daml/splice-dso-governance/daml/Splice/DSO/DecentralizedSynchronizer.daml b/daml/splice-dso-governance/daml/Splice/DSO/DecentralizedSynchronizer.daml index 510ff0b0911..03c332cd4f1 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/DecentralizedSynchronizer.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/DecentralizedSynchronizer.daml @@ -26,7 +26,7 @@ data DsoDecentralizedSynchronizerConfig = DsoDecentralizedSynchronizerConfig wit -- ^ The last allocated synchronizer Id. activeSynchronizerId: Text -- ^ The synchronizer to be used for managing standard DSO and Amulet workflows. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The DSO-level configuration of a synchronizer. This contains the shared parameters -- of the synchronizer. @@ -39,7 +39,7 @@ data SynchronizerConfig = SynchronizerConfig with acsCommitmentReconciliationInterval : Optional Int -- ^ Participants connected to the decentralized synchronizer exchange ACS commitment messages -- every reconciliation interval seconds. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The state of a synchronizer. data SynchronizerState @@ -58,7 +58,7 @@ data SynchronizerState | ExtSynchronizerState -- ^ Extension constructor to work around the current lack of upgrading for variants in Daml 3.0. -- Will serve as the default value in a containing record in case of an extension. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- SV-level configuration @@ -71,13 +71,13 @@ data SequencerIdentityConfig = SequencerIdentityConfig with availableAfter : Optional Time -- ^ Any participant should subscribe this sequencer after this time. -- ^ If not set the sequencer is not yet accessible - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Connection config for a sequencer. data SequencerConnectionConfig = SequencerConnectionConfig with url : Text -- ^ The public accessible url of the sequencer. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Config for a sequencer. data SequencerConfig = SequencerConfig with @@ -90,7 +90,7 @@ data SequencerConfig = SequencerConfig with availableAfter : Optional Time -- ^ Any participant should subscribe this sequencer after this time. -- ^ If not set the sequencer is not yet accessible - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Config for a legacy sequencer, i.e., a migration id that is still up but paused. This is useful to allow validators to catch up. data LegacySequencerConfig = LegacySequencerConfig with @@ -100,26 +100,26 @@ data LegacySequencerConfig = LegacySequencerConfig with -- ^ The id of the sequencer. url : Text -- ^ The public accessible url of the sequencer. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Config for a mediator. data MediatorConfig = MediatorConfig with mediatorId : Text -- ^ The id of the mediator. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Config for a Scan instance. data ScanConfig = ScanConfig with publicUrl : Text -- ^ The publicly accessible URL of the Scan instance. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data PhysicalSynchronizerNodeConfig = PhysicalSynchronizerNodeConfig with sequencer : Optional SequencerConnectionConfig -- ^ The configuration of this sv's optional local sequencer. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A map from synchronizer-ids to the configuration of a sv's node for this synchronizer. type SynchronizerNodeConfigMap = Map.Map Text SynchronizerNodeConfig @@ -151,14 +151,14 @@ data SynchronizerNodeConfig = SynchronizerNodeConfig with physicalSynchronizers: Optional PhysicalSynchronizerNodeConfigMap -- ^ The synchronizer node configs for the currently running physical synchronizers. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) noSynchronizerNodes : SynchronizerNodeConfigMap noSynchronizerNodes = Map.empty data SynchronizerNodeConfigLimits = SynchronizerNodeConfigLimits with cometBft : CometBftConfigLimits - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- default values diff --git a/daml/splice-dso-governance/daml/Splice/DSO/SvState.daml b/daml/splice-dso-governance/daml/Splice/DSO/SvState.daml index 72239fd1f3b..bd188efd679 100644 --- a/daml/splice-dso-governance/daml/Splice/DSO/SvState.daml +++ b/daml/splice-dso-governance/daml/Splice/DSO/SvState.daml @@ -16,13 +16,13 @@ import Splice.Util data ForSv = ForSv with dso : Party svName : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data ForSvNode = ForSvNode with dso : Party sv : Party svName : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Reward collection @@ -38,7 +38,7 @@ data RewardState = RewardState with numRoundsCollected : Int -- ^ Number of rounds for which they collected coupons. lastRoundCollected : Round -- ^ Last round for which they collected reward coupons. numCouponsIssued : Int -- ^ Number of SV reward coupons issued by them for their beneficiaries. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | State of reward collection for a sv identified by their sv name. template SvRewardState with @@ -55,7 +55,7 @@ template SvRewardState with data NodeState = NodeState with synchronizerNodes : SynchronizerNodeConfigMap -- ^ The config for a synchronizer's CometBFT, sequencer, mediator and scan nodes under control of an SV party. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | State of a node managed by an SV operator party. -- @@ -90,7 +90,7 @@ data SvStatus = SvStatus with -- ^ The latest synchronizer time observed on the participant at the time of submission latestOpenRound : Round -- ^ The maximum round number of the OpenMiningRound contracts at the time of submission - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A singleton contract per SV party that is used to regularly submit status reports. -- diff --git a/daml/splice-dso-governance/daml/Splice/DsoBootstrap.daml b/daml/splice-dso-governance/daml/Splice/DsoBootstrap.daml index c3dedfbc78a..de7e6d1a3d1 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoBootstrap.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoBootstrap.daml @@ -18,6 +18,7 @@ import Splice.DSO.DecentralizedSynchronizer import Splice.DsoRules data DsoBootstrap_BootstrapResult = DsoBootstrap_BootstrapResult + deriving (Serializable) -- | A template for bootstrapping DsoRules and AmuletRules. template DsoBootstrap with diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index ff4829a8cda..0a88f5d3906 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -12,6 +12,7 @@ import DA.Foldable (forA_, all) import DA.List as List hiding (all) import DA.Optional (isNone, fromSome, fromOptional, isSome) import qualified DA.Map as Map +import DA.TextMap (TextMap) import qualified DA.Set as Set import qualified DA.Text as T import DA.Time @@ -77,7 +78,7 @@ data ActionRequiringConfirmation dummyUnitField : () -- ^ Extension constructor (and field) to work around the current lack of upgrading for variants in Daml 3.0 -- This on takes care of providing extensibility for the specific variants below. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AmuletRules_ActionRequiringConfirmation = CRARC_MiningRound_StartIssuing AmuletRules_MiningRound_StartIssuing @@ -94,7 +95,7 @@ data AmuletRules_ActionRequiringConfirmation -- ^ Voted action to change the `AmuletConfig`. Not idempotent. | CRARC_StartProcessingRewardsV2 AmuletRules_StartProcessingRewardsV2 -- ^ Automated action to start the processing of rewards where the minting allowances were computed off-ledger. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data DsoRules_ActionRequiringConfirmation = SRARC_AddSv DsoRules_AddSv @@ -121,14 +122,14 @@ data DsoRules_ActionRequiringConfirmation -- ^ Create BootstrapExternalPartyConfigStateInstruction | SRARC_UpdateFeaturedAppRight DsoRules_UpdateFeaturedAppRight -- ^ Update a specific featured app right. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AnsEntryContext_ActionRequiringConfirmation = ANSRARC_CollectInitialEntryPayment AnsEntryContext_CollectInitialEntryPayment -- ^ Automated action to collect initial payment of an ans entry. | ANSRARC_RejectEntryInitialPayment AnsEntryContext_RejectEntryInitialPayment -- ^ Automated action to reject initial payment of an ans entry. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Information about SVs relevant to DSO governance. data SvInfo = SvInfo with @@ -136,13 +137,13 @@ data SvInfo = SvInfo with joinedAsOfRound : Round -- ^ Round in which the SV joined svRewardWeight : Int -- ^ Weight of the SV in the SV reward distribution. participantId : Text -- ^ Participant ID of the SV, stored here as PartyToParticipant mappings are tracked via state on the DsoRules + SvOnboardingConfirmed contracts. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Information about offboarded svs data OffboardedSvInfo = OffboardedSvInfo with name : Text -- ^ Human-readable name; must be unique. participantId : Text -- ^ Participant ID of the offboarded SV. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data DsoSummary = DsoSummary with dsoDelegate : Party @@ -150,7 +151,7 @@ data DsoSummary = DsoSummary with numSvs : Int requiredNumVotes : Int -- ^ The number of votes required for considering a confirmation, or a request for a vote - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Choice return types ------------------------- @@ -158,203 +159,259 @@ data DsoSummary = DsoSummary with -- then be extended in future versions. data Confirmation_ExpireResult = Confirmation_ExpireResult + deriving (Serializable) data DsoRules_AddSvResult = DsoRules_AddSvResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_OffboardSvResult = DsoRules_OffboardSvResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_SubmitStatusReportResult = DsoRules_SubmitStatusReportResult with newReport : ContractId SvStatusReport + deriving (Serializable) data DsoRules_AddConfirmedSvResult = DsoRules_AddConfirmedSvResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_SetSynchronizerNodeConfigResult = DsoRules_SetSynchronizerNodeConfigResult with svNodeState : ContractId SvNodeState + deriving (Serializable) data DsoRules_RequestElectionResult = DsoRules_RequestElectionResult with electionRequestCid : ContractId ElectionRequest + deriving (Serializable) data DsoRules_ElectDsoDelegateResult = DsoRules_ElectDsoDelegateResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_ArchiveOutdatedElectionRequestResult = DsoRules_ArchiveOutdatedElectionRequestResult + deriving (Serializable) data DsoRules_ConfirmActionResult = DsoRules_ConfirmActionResult with confirmation : ContractId Confirmation + deriving (Serializable) data DsoRules_ExecuteConfirmedActionResult = DsoRules_ExecuteConfirmedActionResult + deriving (Serializable) data DsoRules_ExpireStaleConfirmationResult = DsoRules_ExpireStaleConfirmationResult + deriving (Serializable) data DsoRules_RequestVoteResult = DsoRules_RequestVoteResult with voteRequest : ContractId VoteRequest + deriving (Serializable) data DsoRules_CastVoteResult = DsoRules_CastVoteResult with voteRequest : ContractId VoteRequest + deriving (Serializable) data DsoRules_UpdateAmuletPriceVoteResult = DsoRules_UpdateAmuletPriceVoteResult with amuletPriceVote : ContractId AmuletPriceVote + deriving (Serializable) data DsoRules_GarbageCollectAmuletPriceVotesResult = DsoRules_GarbageCollectAmuletPriceVotesResult + deriving (Serializable) data DsoRules_SetConfigResult = DsoRules_SetConfigResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_UpdateSvRewardWeightResult = DsoRules_UpdateSvRewardWeightResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_GrantFeaturedAppRightResult = DsoRules_GrantFeaturedAppRightResult with featuredAppRight : ContractId FeaturedAppRight + deriving (Serializable) data DsoRules_RevokeFeaturedAppRightResult = DsoRules_RevokeFeaturedAppRightResult + deriving (Serializable) data DsoRules_UpdateFeaturedAppRightResult = DsoRules_UpdateFeaturedAppRightResult with result : FeaturedAppRight_UpdateResult + deriving (Serializable) data DsoRules_OnboardValidatorResult = DsoRules_OnboardValidatorResult with validatorLicense : ContractId ValidatorLicense + deriving (Serializable) data DsoRules_MergeValidatorLicenseResult = DsoRules_MergeValidatorLicenseResult with validatorLicense : ContractId ValidatorLicense + deriving (Serializable) data DsoRules_StartSvOnboardingResult = DsoRules_StartSvOnboardingResult with onboardingRequest : ContractId SvOnboardingRequest + deriving (Serializable) data DsoRules_ExpireSvOnboardingRequestResult = DsoRules_ExpireSvOnboardingRequestResult + deriving (Serializable) data DsoRules_ArchiveSvOnboardingRequestResult = DsoRules_ArchiveSvOnboardingRequestResult + deriving (Serializable) data DsoRules_ConfirmSvOnboardingResult = DsoRules_ConfirmSvOnboardingResult with onboardingConfirmed : ContractId SvOnboardingConfirmed + deriving (Serializable) data DsoRules_ExpireSvOnboardingConfirmedResult = DsoRules_ExpireSvOnboardingConfirmedResult with newDsoRules : ContractId DsoRules + deriving (Serializable) data DsoRules_Amulet_ExpireTransferInstructionsResult = DsoRules_Amulet_ExpireTransferInstructionsResult with result : AmuletRules_Amulet_ExpireTransferInstructionsResult - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data DsoRules_Amulet_ExpireResult = DsoRules_Amulet_ExpireResult with expireSum : AmuletExpireSummary + deriving (Serializable) data DsoRules_Amulet_ExpireV2Result = DsoRules_Amulet_ExpireV2Result with - expireSum : AmuletExpireV2Summary + expireSum : AmuletExpireV2Summary + deriving (Serializable) data DsoRules_LockedAmulet_ExpireAmuletResult = DsoRules_LockedAmulet_ExpireAmuletResult with expireSum : AmuletExpireSummary + deriving (Serializable) data DsoRules_LockedAmulet_ExpireAmuletV2Result = DsoRules_LockedAmulet_ExpireAmuletV2Result with expireSum : AmuletExpireV2Summary + deriving (Serializable) data DsoRules_AdvanceOpenMiningRoundsResult = DsoRules_AdvanceOpenMiningRoundsResult with summarizingRound: ContractId SummarizingMiningRound openRound : ContractId OpenMiningRound + deriving (Serializable) data DsoRules_UpdateExternalPartyConfigStatesResult = DsoRules_UpdateExternalPartyConfigStatesResult with newExternalPartyConfigStateCid : ContractId ExternalPartyConfigState - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data DsoRules_CollectEntryRenewalPaymentResult = DsoRules_CollectEntryRenewalPaymentResult with ansEntry : ContractId AnsEntry subscriptionState : ContractId SubscriptionIdleState + deriving (Serializable) data DsoRules_ExpireAnsEntryResult = DsoRules_ExpireAnsEntryResult + deriving (Serializable) data DsoRules_ExpireSubscriptionResult = DsoRules_ExpireSubscriptionResult + deriving (Serializable) data DsoRules_TerminateSubscriptionResult = DsoRules_TerminateSubscriptionResult + deriving (Serializable) data DsoRules_ReceiveSvRewardCouponResult = DsoRules_ReceiveSvRewardCouponResult with svRewardState : ContractId SvRewardState svRewardCoupons : [ContractId SvRewardCoupon] + deriving (Serializable) data DsoRules_ClaimExpiredRewardsResult = DsoRules_ClaimExpiredRewardsResult with unclaimedReward: Optional (ContractId UnclaimedReward) + deriving (Serializable) data DsoRules_ClaimExpiredRewardsV2Result = DsoRules_ClaimExpiredRewardsV2Result with result : AmuletRules_ClaimExpiredRewardsV2Result + deriving (Serializable) data DsoRules_StartProcessingRewardsV2Result = DsoRules_StartProcessingRewardsV2Result with result : AmuletRules_StartProcessingRewardsV2Result + deriving (Serializable) data DsoRules_ProcessRewardsV2_ProcessBatchResult = DsoRules_ProcessRewardsV2_ProcessBatchResult with result : RewardAccountingV2.ProcessRewardsV2_ProcessBatchResult + deriving (Serializable) data DsoRules_UnhideRewardCouponsV2Result = DsoRules_UnhideRewardCouponsV2Result with result : AmuletRules_UnhideRewardCouponsV2Result + deriving (Serializable) data DsoRules_ArchiveDryRunRewardAccountingV2Result = DsoRules_ArchiveDryRunRewardAccountingV2Result with result : AmuletRules_ArchiveDryRunRewardAccountingV2Result + deriving (Serializable) data DsoRules_MergeUnclaimedRewardsResult = DsoRules_MergeUnclaimedRewardsResult with unclaimedReward: ContractId UnclaimedReward + deriving (Serializable) data DsoRules_MergeUnclaimedDevelopmentFundCouponsResult = DsoRules_MergeUnclaimedDevelopmentFundCouponsResult with result : AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult + deriving (Serializable) data DsoRules_ExpireDevelopmentFundCouponResult = DsoRules_ExpireDevelopmentFundCouponResult with result : DevelopmentFundCoupon_DsoExpireResult + deriving (Serializable) data DsoRules_MiningRound_CloseResult = DsoRules_MiningRound_CloseResult with closedRound : ContractId ClosedMiningRound + deriving (Serializable) data DsoRules_MergeMemberTrafficContractsResult = DsoRules_MergeMemberTrafficContractsResult with memberTraffic : ContractId MemberTraffic + deriving (Serializable) data DsoRules_MergeSvRewardStateResult = DsoRules_MergeSvRewardStateResult with svRewardState : ContractId SvRewardState + deriving (Serializable) data DsoRules_PruneAmuletConfigScheduleResult = DsoRules_PruneAmuletConfigScheduleResult with amuletRulesCid : ContractId AmuletRules + deriving (Serializable) data DsoRules_CreateExternalPartyAmuletRulesResult = DsoRules_CreateExternalPartyAmuletRulesResult with externalPartyAmuletRulesCid : ContractId ExternalPartyAmuletRules - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data DsoRules_CreateTransferCommandCounterResult = DsoRules_CreateTransferCommandCounterResult with transferCommandCounterCid : ContractId TransferCommandCounter - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data DsoRules_ExpireTransferPreapprovalResult = DsoRules_ExpireTransferPreapprovalResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data DsoRules_AmuletRules_ConvertFeaturedAppActivityMarkersResult = DsoRules_AmuletRules_ConvertFeaturedAppActivityMarkersResult with result : AmuletRules_ConvertFeaturedAppActivityMarkersResult + deriving (Serializable) data DsoRules_CreateUnallocatedUnclaimedActivityRecordResult = DsoRules_CreateUnallocatedUnclaimedActivityRecordResult with unallocatedUnclaimedActivityRecordCid : ContractId UnallocatedUnclaimedActivityRecord + deriving (Serializable) data DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult = DsoRules_AllocateUnallocatedUnclaimedActivityRecordResult with unclaimedActivityRecordCid : ContractId UnclaimedActivityRecord optUnclaimedRewardCid : Optional (ContractId UnclaimedReward) + deriving (Serializable) data DsoRules_ExpireUnallocatedUnclaimedActivityRecordResult = DsoRules_ExpireUnallocatedUnclaimedActivityRecordResult + deriving (Serializable) data DsoRules_ExpireUnclaimedActivityRecordResult = DsoRules_ExpireUnclaimedActivityRecordResult with unclaimedRewardCid : ContractId UnclaimedReward + deriving (Serializable) data DsoRules_CreateBootstrapExternalPartyConfigStateInstructionResult = DsoRules_CreateBootstrapExternalPartyConfigStateInstructionResult with instructionCid : ContractId BootstrapExternalPartyConfigStateInstruction - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data DsoRules_ExpireAmuletAllocationsResult = DsoRules_ExpireAmuletAllocationsResult with result : ExternalPartyAmuletRules_ExpireAmuletAllocationsResult - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data DsoRules_ExpireAmuletAllocationsV2Result = DsoRules_ExpireAmuletAllocationsV2Result with result : ExternalPartyAmuletRules_ExpireAmuletAllocationsV2Result - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Workflow templates @@ -367,7 +424,7 @@ data ElectionRequestReason | ExtElectionRequestReason with dummyUnitField : () -- ^ Extension constructor (and field) to work around the current lack of upgrading for variants in Daml 3.0 - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | __Deprecated__ in favor of delegateless automation. -- @@ -438,9 +495,10 @@ data DsoRules_CloseVoteRequestResult = DsoRules_CloseVoteRequestResult with offboardedVoters : [Text] -- ^ SVs that voted but were offboarded before the vote completed. abstainingSvs : [Text] -- ^ SVs that did not vote. outcome : VoteRequestOutcome -- ^ The final result of the vote. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data DsoRules_BootstrapExternalPartyConfigStateResult = DsoRules_BootstrapExternalPartyConfigStateResult + deriving (Serializable) data VoteRequestOutcome = VRO_AcceptedButActionFailed with -- No longer used after removing try/catch in DsoRules_CloseVoteRequest. @@ -452,7 +510,7 @@ data VoteRequestOutcome | ExtVoteRequestOutcome with dummyUnitField : () -- ^ Extension constructor (and field) to work around the current lack of upgrading for variants in Daml 3.0 - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- -- | A vote cast by an SV. data Vote = Vote with @@ -463,12 +521,12 @@ data Vote = Vote with -- ^ The reason for voting this way. optCastAt : Optional Time -- ^ The time when the vote was cast. Used to rate-limit the casting of votes. - deriving(Eq, Show) + deriving(Eq, Show, Serializable) data Reason = Reason with url : Text -- ^ Url pointing to additional background on the reason, e.g., using a https://w3c-ccg.github.io/hashlink/ body : Text -- ^ Freeform text intended for human consumption. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data DsoRulesConfig = DsoRulesConfig with @@ -486,7 +544,8 @@ data DsoRulesConfig = DsoRulesConfig with nextScheduledSynchronizerUpgrade: Optional SynchronizerUpgradeSchedule voteCooldownTime : Optional RelTime -- ^ The minimum time between two votes by the same SV. nextScheduledLogicalSynchronizerUpgrade: Optional LogicalSynchronizerUpgradeSchedule - deriving (Eq, Show) + svOperationsSwitchOverTimes : Optional (TextMap Time) -- ^ Timepoints for synchronized switch-overs of amulet functionality both on and off-ledger. + deriving (Eq, Show, Serializable) -- | Read the `voteCooldownTime` from the `DsoRulesConfig` with a default value of 1 minute. getVoteCooldownTime : DsoRulesConfig -> RelTime @@ -498,7 +557,7 @@ data SynchronizerUpgradeSchedule = SynchronizerUpgradeSchedule with -- ^ The time at which the migration is scheduled to start. migrationId : Int -- ^ The incremental integer ID of the migration. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data LogicalSynchronizerUpgradeSchedule = LogicalSynchronizerUpgradeSchedule with @@ -511,12 +570,12 @@ data LogicalSynchronizerUpgradeSchedule = LogicalSynchronizerUpgradeSchedule -- ^ The serial of the new physical synchronizer. Usually just the serial of the old physical synchronizer + 1. newPhysicalSynchronizerProtocolVersion : Text -- ^ The protocol version of the new physical synchronizer - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data TrafficState = TrafficState with consumedTraffic: Int -- ^ Bytes of extra traffic consumed before the decentralized synchronizer was bootstrapped. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) template DsoRules with dso : Party @@ -1834,7 +1893,7 @@ data VotingState a = VotingState with loosers : Set.Set a -- ^ Loosers are tracked explicitly and removed lazily to avoid the cubic runtime -- that would result from filtering the remaining rankings eagerly. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Wellformedness checks @@ -2052,6 +2111,7 @@ instance Patchable DsoRulesConfig where nextScheduledSynchronizerUpgrade = patch new.nextScheduledSynchronizerUpgrade base.nextScheduledSynchronizerUpgrade current.nextScheduledSynchronizerUpgrade nextScheduledLogicalSynchronizerUpgrade = patch new.nextScheduledLogicalSynchronizerUpgrade base.nextScheduledLogicalSynchronizerUpgrade current.nextScheduledLogicalSynchronizerUpgrade voteCooldownTime = patch new.voteCooldownTime base.voteCooldownTime current.voteCooldownTime + svOperationsSwitchOverTimes = patch new.svOperationsSwitchOverTimes base.svOperationsSwitchOverTimes current.svOperationsSwitchOverTimes instance Patchable SynchronizerUpgradeSchedule where patch new base current = SynchronizerUpgradeSchedule with diff --git a/daml/splice-dso-governance/daml/Splice/SvOnboarding.daml b/daml/splice-dso-governance/daml/Splice/SvOnboarding.daml index 6584666a4c4..635592a6dbc 100644 --- a/daml/splice-dso-governance/daml/Splice/SvOnboarding.daml +++ b/daml/splice-dso-governance/daml/Splice/SvOnboarding.daml @@ -8,8 +8,10 @@ import Splice.Types import Splice.Util data SvOnboardingRequest_ExpireResult = SvOnboardingRequest_ExpireResult + deriving (Serializable) data SvOnboardingConfirmed_ExpireResult = SvOnboardingConfirmed_ExpireResult + deriving (Serializable) -- | Template used by the SVs to collect confirmations for the onboarding of an SV candidate. -- The existence of this contract triggers SV automation that creates confirmation contracts for diff --git a/daml/splice-util-batched-markers-test/daml.yaml b/daml/splice-util-batched-markers-test/daml.yaml index c6b791be934..9537ad8e0d2 100644 --- a/daml/splice-util-batched-markers-test/daml.yaml +++ b/daml/splice-util-batched-markers-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-util-batched-markers-test source: daml -version: 1.0.9 +version: 1.0.10 dependencies: - daml-prim - daml-script @@ -13,6 +13,7 @@ data-dependencies: - ../splice-util-batched-markers/.daml/dist/splice-util-batched-markers-current.dar - ../../token-standard/splice-token-standard-v1-test/.daml/dist/splice-token-standard-v1-test-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-util-batched-markers/daml.yaml b/daml/splice-util-batched-markers/daml.yaml index b046711c379..c00b8a1d1ad 100644 --- a/daml/splice-util-batched-markers/daml.yaml +++ b/daml/splice-util-batched-markers/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-util-batched-markers source: daml -version: 1.0.3 +version: 1.0.4 dependencies: - daml-prim - daml-stdlib @@ -9,6 +9,7 @@ data-dependencies: - ../dars/splice-api-featured-app-v1-1.0.0.dar - ../dars/splice-api-featured-app-v2-1.0.0.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-util-batched-markers/daml/Splice/Util/FeaturedApp/BatchedMarkersProxy.daml b/daml/splice-util-batched-markers/daml/Splice/Util/FeaturedApp/BatchedMarkersProxy.daml index 0956736822f..8c0117c45ee 100644 --- a/daml/splice-util-batched-markers/daml/Splice/Util/FeaturedApp/BatchedMarkersProxy.daml +++ b/daml/splice-util-batched-markers/daml/Splice/Util/FeaturedApp/BatchedMarkersProxy.daml @@ -11,23 +11,25 @@ import qualified Splice.Api.FeaturedAppRightV2 as FeaturedAppRightV2 data BatchedMarkersProxy_CreateMarkersResult = BatchedMarkersProxy_CreateMarkersResult with results : [[FeaturedAppRightV1.FeaturedAppRight_CreateActivityMarkerResult]] + deriving (Serializable) data BatchedMarkersProxy_CreateMarkersV2Result = BatchedMarkersProxy_CreateMarkersV2Result with results : [FeaturedAppRightV2.FeaturedAppRight_CreateActivityMarkerResult] + deriving (Serializable) data RewardBatch = RewardBatch with beneficiaries : [FeaturedAppRightV1.AppRewardBeneficiary] numMarkers : Int - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data RewardBatchV2 = RewardBatchV2 with beneficiaries : [FeaturedAppRightV2.AppRewardBeneficiary] markerWeight : Decimal -- ^ The total weight of the markers that should be created. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) template BatchedMarkersProxy with provider : Party diff --git a/daml/splice-util-featured-app-proxies-test/daml.yaml b/daml/splice-util-featured-app-proxies-test/daml.yaml index c6baf262822..b57e64cc1bd 100644 --- a/daml/splice-util-featured-app-proxies-test/daml.yaml +++ b/daml/splice-util-featured-app-proxies-test/daml.yaml @@ -11,7 +11,7 @@ description: | are normal .dar files and can be shared by copying the .dars. (TODO(#594): remove this limitation) source: daml -version: 1.0.15 +version: 1.0.16 dependencies: - daml-prim - daml-stdlib @@ -26,6 +26,7 @@ data-dependencies: - ../dars/splice-api-featured-app-v1-1.0.0.dar - ../splice-util-featured-app-proxies/.daml/dist/splice-util-featured-app-proxies-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-util-token-standard-wallet-test/daml.yaml b/daml/splice-util-token-standard-wallet-test/daml.yaml index 192259cf3de..2a519cf2967 100644 --- a/daml/splice-util-token-standard-wallet-test/daml.yaml +++ b/daml/splice-util-token-standard-wallet-test/daml.yaml @@ -4,7 +4,7 @@ sdk-version: 3.5.2 name: splice-util-token-standard-wallet-test source: daml -version: 1.0.10 +version: 1.0.11 dependencies: - daml-prim - daml-stdlib @@ -33,6 +33,7 @@ data-dependencies: - ../../token-standard/examples/splice-test-token-v2/.daml/dist/splice-test-token-v2-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-util-token-standard-wallet/daml.yaml b/daml/splice-util-token-standard-wallet/daml.yaml index b371ec0e9e2..28ed547d267 100644 --- a/daml/splice-util-token-standard-wallet/daml.yaml +++ b/daml/splice-util-token-standard-wallet/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-util-token-standard-wallet source: daml -version: 1.1.0 +version: 1.1.1 dependencies: - daml-prim - daml-stdlib @@ -20,6 +20,7 @@ data-dependencies: - ../../token-standard/splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/BatchingUtilityV2.daml b/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/BatchingUtilityV2.daml index fbe19bffcde..bdf22e31d94 100644 --- a/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/BatchingUtilityV2.daml +++ b/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/BatchingUtilityV2.daml @@ -55,12 +55,12 @@ import Splice.TokenStandard.Utils hiding (require) data ScopedAccount = ScopedAccount with admin : Party account : V2.Account - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) -- | Map of holdings, keyed by admin, account, and instrument id. data HoldingMap = HoldingMap with byAdminAndAccount : Map.Map ScopedAccount (TextMap.TextMap [ContractId V2.Holding]) - deriving (Eq, Show) + deriving (Eq, Show, Serializable) instance Monoid HoldingMap where mempty = HoldingMap with byAdminAndAccount = Map.empty @@ -110,7 +110,7 @@ getHoldingsForInstrument account instrumentId (HoldingMap inputHoldingMap) = data ChoiceCall arg = ChoiceCall with cid : AnyContractId arg : arg - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A token standard action executed by a token standard wallet. data TokenStandardAction @@ -136,7 +136,7 @@ data TokenStandardAction | TSA_AllocationRequest_AcceptV2 (ChoiceCall V2.AllocationRequest_Accept) | TSA_AllocationRequest_RejectV2 (ChoiceCall V2.AllocationRequest_Reject) - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The result of a token standard action. data TokenStandardActionResult @@ -149,7 +149,7 @@ data TokenStandardActionResult | TSAR_AllocationResultV2 V2.AllocationResult | TSAR_AllocationRequest_AcceptV2Result V2.AllocationRequest_AcceptResult | TSAR_AllocationRequest_RejectV2Result V2.AllocationRequest_RejectResult - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The result of execution a batch of token standard actions. data BatchingUtility_ExecuteBatchResult = BatchingUtility_ExecuteBatchResult with @@ -157,7 +157,7 @@ data BatchingUtility_ExecuteBatchResult = BatchingUtility_ExecuteBatchResult wit -- ^ Results are delivered in the same order as the actions. outputHoldingMap : HoldingMap -- ^ Map of holdings after executing the actions in th batch. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Batching utility diff --git a/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/MergeDelegation.daml b/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/MergeDelegation.daml index 016ce455800..9f75a189fb2 100644 --- a/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/MergeDelegation.daml +++ b/daml/splice-util-token-standard-wallet/daml/Splice/Util/Token/Wallet/MergeDelegation.daml @@ -34,13 +34,13 @@ import Splice.Api.FeaturedAppRightV1 data TransferCall = TransferCall with factoryCid : ContractId TransferFactory choiceArg : TransferFactory_Transfer - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A call to create featured app markers using a featured app right. data FeaturedAppRightCall = FeaturedAppRightCall with appRightCid : ContractId FeaturedAppRight beneficiaries : [AppRewardBeneficiary] - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The right for a operator like the wallet app operator to merge token -- standard holdings on behalf of a token owner. @@ -147,17 +147,17 @@ data MergeDelegation_MergeResult = MergeDelegation_MergeResult with optMergeTransferResult : Optional TransferInstructionResult optExtraTransferResult : Optional TransferInstructionResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Dedicated record type to simplify upgrading data MergeDelegation_RejectResult = MergeDelegation_RejectResult with result : () - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Dedicated record type to simplify upgrading data MergeDelegation_WithdrawResult = MergeDelegation_WithdrawResult with result : () - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Batch merge delegation utility @@ -166,7 +166,7 @@ data MergeDelegation_WithdrawResult = MergeDelegation_WithdrawResult with data MergeDelegationCall = MergeDelegationCall with delegationCid : ContractId MergeDelegation choiceArg : MergeDelegation_Merge - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Utility contract for a operator to execute a batch of merge calls with -- proper threading of the operator's change holdings between the calls that @@ -191,7 +191,7 @@ data BatchMergeUtility_BatchMergeResult = BatchMergeUtility_BatchMergeResult wit -- ^ Results of each individual merge delegation call. operatorChangeMap : InstrumentHoldingMap -- ^ Change holdings for the operator after executing extra transfers. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) type InstrumentHoldingMap = M.Map InstrumentId [ContractId Holding] @@ -261,17 +261,17 @@ template MergeDelegationProposal data MergeDelegationProposal_AcceptResult = MergeDelegationProposal_AcceptResult with mergeDelegationCid : ContractId MergeDelegation - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Dedicated record type to simplify upgrading data MergeDelegationProposal_RejectResult = MergeDelegationProposal_RejectResult with result : () - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Dedicated record type to simplify upgrading data MergeDelegationProposal_WithdrawResult = MergeDelegationProposal_WithdrawResult with result : () - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- utility functions diff --git a/daml/splice-util/daml.yaml b/daml/splice-util/daml.yaml index c0eb503b58d..7e806182654 100644 --- a/daml/splice-util/daml.yaml +++ b/daml/splice-util/daml.yaml @@ -1,12 +1,13 @@ sdk-version: 3.5.2 name: splice-util source: daml -version: 0.1.7 +version: 0.1.8 dependencies: - daml-prim - daml-stdlib data-dependencies: [] build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-util/daml/Splice/Util.daml b/daml/splice-util/daml/Splice/Util.daml index aa9a17a0cdb..4039af28344 100644 --- a/daml/splice-util/daml/Splice/Util.daml +++ b/daml/splice-util/daml/Splice/Util.daml @@ -39,6 +39,8 @@ import qualified DA.Set as Set import DA.Map (Map) import qualified DA.Map as Map import DA.List() +import DA.TextMap (TextMap) +import DA.TextMap qualified as TextMap -- | Require that a contract-id refers to a specific contract. requireMatchingContract : (Eq t, Show t, HasFetch t) => ContractId t -> t -> Update () @@ -231,6 +233,28 @@ instance (Ord k, Patchable v) => Patchable (Map k v) where base keysToRemove = mapDifference new base +textMapDifference : TextMap a -> TextMap a -> TextMap Text +textMapDifference = TextMap.merge (\_ _ -> None) (\k _ -> Some k) (\_ _ _ -> None) + +instance (Patchable a) => Patchable (TextMap a) where + patch new base current = + TextMap.union toAdd (TextMap.filterWithKey (\key _ -> not (TextMap.member key keysToRemove)) current) + where + toAdd = TextMap.merge + -- Keys only in `new`: Add them to the result + (\_ value -> Some value) + -- Keys only in `base`: Remove them from the result (i.e., return `None`) + (\_key _ -> None) + -- Keys in both `new` and `base`: Use `patch` to merge with `base` + (\key newValue baseValue -> + case TextMap.lookup key current of + None -> None -- Value got removed by concurrent request + Some currentValue -> Some (patch newValue baseValue currentValue) + ) + new + base + keysToRemove = textMapDifference new base + instance Patchable a => Patchable (Optional a) where patch new base current = case (base, new) of diff --git a/daml/splice-validator-lifecycle-test/daml.yaml b/daml/splice-validator-lifecycle-test/daml.yaml index facd84453d0..41dc915edcf 100644 --- a/daml/splice-validator-lifecycle-test/daml.yaml +++ b/daml/splice-validator-lifecycle-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-validator-lifecycle-test source: daml -version: 0.1.8 +version: 0.1.9 dependencies: - daml-prim - daml-stdlib @@ -10,6 +10,7 @@ data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-validator-lifecycle/.daml/dist/splice-validator-lifecycle-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-validator-lifecycle/daml.yaml b/daml/splice-validator-lifecycle/daml.yaml index 57f41113078..78f981b0fa7 100644 --- a/daml/splice-validator-lifecycle/daml.yaml +++ b/daml/splice-validator-lifecycle/daml.yaml @@ -1,13 +1,14 @@ sdk-version: 3.5.2 name: splice-validator-lifecycle source: daml -version: 0.1.8 +version: 0.1.9 dependencies: - daml-prim - daml-stdlib data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-validator-lifecycle/daml/Splice/ValidatorOnboarding.daml b/daml/splice-validator-lifecycle/daml/Splice/ValidatorOnboarding.daml index a9552faeb30..3371de4f7dd 100644 --- a/daml/splice-validator-lifecycle/daml/Splice/ValidatorOnboarding.daml +++ b/daml/splice-validator-lifecycle/daml/Splice/ValidatorOnboarding.daml @@ -9,8 +9,10 @@ import Splice.Util data ValidatorOnboarding_MatchResult = ValidatorOnboarding_MatchResult with usedSecret : ContractId UsedSecret + deriving (Serializable) data ValidatorOnboarding_ExpireResult = ValidatorOnboarding_ExpireResult + deriving (Serializable) -- | Template used by a sponsoring SV node to keep track of the onboarding of a validator candidate. -- We use a secret for authenticating the candidate once he has set up his participant. diff --git a/daml/splice-wallet-payments/daml.yaml b/daml/splice-wallet-payments/daml.yaml index 9308e42c322..49de4f47ade 100644 --- a/daml/splice-wallet-payments/daml.yaml +++ b/daml/splice-wallet-payments/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-wallet-payments source: daml -version: 0.1.22 +version: 0.1.23 dependencies: - daml-prim - daml-stdlib @@ -11,6 +11,7 @@ data-dependencies: - ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar - ../../token-standard/splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-wallet-payments/daml/Splice/Wallet/Payment.daml b/daml/splice-wallet-payments/daml/Splice/Wallet/Payment.daml index ce84e00db08..7d6ecf174bb 100644 --- a/daml/splice-wallet-payments/daml/Splice/Wallet/Payment.daml +++ b/daml/splice-wallet-payments/daml/Splice/Wallet/Payment.daml @@ -22,44 +22,50 @@ data Unit | ExtUnit -- ^ Extension constructor to work around the current lack of upgrading for variants in Daml 3.0. -- Will serve as the default value in a containing record in case of an extension. - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) data AppPaymentRequest_ExpireResult = AppPaymentRequest_ExpireResult with - terminatedAppPayment : ContractId TerminatedAppPayment + terminatedAppPayment : ContractId TerminatedAppPayment + deriving (Serializable) data AppPaymentRequest_AcceptResult = AppPaymentRequest_AcceptResult with - acceptedPayment : ContractId AcceptedAppPayment - senderChangeAmulet : Optional (ContractId Amulet) + acceptedPayment : ContractId AcceptedAppPayment + senderChangeAmulet : Optional (ContractId Amulet) + deriving (Serializable) data AppPaymentRequest_WithdrawResult = AppPaymentRequest_WithdrawResult with - terminatedAppPayment : ContractId TerminatedAppPayment + terminatedAppPayment : ContractId TerminatedAppPayment + deriving (Serializable) data AppPaymentRequest_RejectResult = AppPaymentRequest_RejectResult with - terminatedAppPayment : ContractId TerminatedAppPayment + terminatedAppPayment : ContractId TerminatedAppPayment + deriving (Serializable) data AcceptedAppPayment_RejectResult = AcceptedAppPayment_RejectResult with - amulet : AmuletCreateSummary (ContractId Amulet) + amulet : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data AcceptedAppPayment_ExpireResult = AcceptedAppPayment_ExpireResult with - amulet : AmuletCreateSummary (ContractId Amulet) + amulet : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data PaymentAmount = PaymentAmount with amount : Decimal unit : Unit - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) data ReceiverAmount = ReceiverAmount with receiver : Party amount : PaymentAmount - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) data ReceiverAmuletAmount = ReceiverAmuletAmount with receiver : Party amuletAmount : Decimal - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) paymentAmountToAmulet : Party -> OpenMiningRound -> PaymentAmount -> Update Decimal paymentAmountToAmulet dso openMiningRound pq = @@ -182,7 +188,7 @@ template TerminatedAppPayment signatory sender, provider, receivers data ReceiverAmulet = ReceiverAmulet with receiver : Party, lockedAmulet : ContractId LockedAmulet - deriving (Eq, Show) + deriving (Eq, Show, Serializable) unzipReceiverAmulets : [ReceiverAmulet] -> ([Party], [ContractId LockedAmulet]) unzipReceiverAmulets = foldr (\ReceiverAmulet{..} (rs,cs) -> (receiver :: rs, lockedAmulet :: cs)) ([], []) @@ -200,6 +206,7 @@ mkReceiverOutput receiverAmount = data AcceptedAppPayment_CollectResult = AcceptedAppPayment_CollectResult with receiverAmulets: [(Party, ContractId Amulet)] + deriving (Serializable) template AcceptedAppPayment with diff --git a/daml/splice-wallet-payments/daml/Splice/Wallet/Subscriptions.daml b/daml/splice-wallet-payments/daml/Splice/Wallet/Subscriptions.daml index d1292f0d5c6..d8049611650 100644 --- a/daml/splice-wallet-payments/daml/Splice/Wallet/Subscriptions.daml +++ b/daml/splice-wallet-payments/daml/Splice/Wallet/Subscriptions.daml @@ -49,56 +49,69 @@ data SubscriptionData = SubscriptionData provider : Party -- ^ The app provider. dso : Party description : Text - deriving (Show, Eq) + deriving (Show, Eq, Serializable) subscriptionSignatories : SubscriptionData -> [Party] subscriptionSignatories SubscriptionData{sender, receiver, provider} = [sender, receiver, provider] data Subscription_ArchiveResult = Subscription_ArchiveResult with - terminatedSubscription : ContractId TerminatedSubscription + terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data SubscriptionRequest_AcceptAndMakePaymentResult = SubscriptionRequest_AcceptAndMakePaymentResult with - subscriptionPayment : ContractId SubscriptionInitialPayment - senderChange : Optional (ContractId Amulet) + subscriptionPayment : ContractId SubscriptionInitialPayment + senderChange : Optional (ContractId Amulet) + deriving (Serializable) data SubscriptionRequest_WithdrawResult = SubscriptionRequest_WithdrawResult with - terminatedSubscription : ContractId TerminatedSubscription + terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data SubscriptionRequest_RejectResult = SubscriptionRequest_RejectResult with - terminatedSubscription : ContractId TerminatedSubscription + terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data SubscriptionInitialPayment_CollectResult = SubscriptionInitialPayment_CollectResult with - subscription : ContractId Subscription - subscriptionState : ContractId SubscriptionIdleState - amulet : ContractId Amulet + subscription : ContractId Subscription + subscriptionState : ContractId SubscriptionIdleState + amulet : ContractId Amulet + deriving (Serializable) data SubscriptionInitialPayment_RejectResult = SubscriptionInitialPayment_RejectResult with - amuletSum : AmuletCreateSummary (ContractId Amulet) + amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data SubscriptionInitialPayment_ExpireResult = SubscriptionInitialPayment_ExpireResult with - amuletSum : AmuletCreateSummary (ContractId Amulet) + amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data SubscriptionIdleState_ExpireSubscriptionResult = SubscriptionIdleState_ExpireSubscriptionResult with - terminatedSubscription : ContractId TerminatedSubscription + terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data SubscriptionIdleState_CancelSubscriptionResult = SubscriptionIdleState_CancelSubscriptionResult with - terminatedSubscription : ContractId TerminatedSubscription + terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data SubscriptionIdleState_MakePaymentResult = SubscriptionIdleState_MakePaymentResult with - subscriptionPayment : ContractId SubscriptionPayment - senderChange : Optional (ContractId Amulet) + subscriptionPayment : ContractId SubscriptionPayment + senderChange : Optional (ContractId Amulet) + deriving (Serializable) data SubscriptionPayment_CollectResult = SubscriptionPayment_CollectResult with - subscriptionState : ContractId SubscriptionIdleState - amulet : ContractId Amulet + subscriptionState : ContractId SubscriptionIdleState + amulet : ContractId Amulet + deriving (Serializable) data SubscriptionPayment_RejectResult = SubscriptionPayment_RejectResult with - subscriptionState : ContractId SubscriptionIdleState - amuletSum : AmuletCreateSummary (ContractId Amulet) + subscriptionState : ContractId SubscriptionIdleState + amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) data SubscriptionPayment_ExpireResult = SubscriptionPayment_ExpireResult with - subscriptionState : ContractId SubscriptionIdleState - amuletSum : AmuletCreateSummary (ContractId Amulet) + subscriptionState : ContractId SubscriptionIdleState + amuletSum : AmuletCreateSummary (ContractId Amulet) + deriving (Serializable) -- | Main subscription object. @@ -136,7 +149,7 @@ data SubscriptionPayData = SubscriptionPayData paymentAmount : PaymentAmount -- ^ What amount of amulet is due on each interval. paymentInterval : RelTime -- ^ At which intervals payments should be made. paymentDuration : RelTime -- ^ The time available to the sender to initiate a payment; they can initiate the payment this much before the end of the current interval. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- for ensure assertions payDataIsValid: SubscriptionPayData -> Bool diff --git a/daml/splice-wallet-test/daml.yaml b/daml/splice-wallet-test/daml.yaml index 16dc4ca4244..897aae0916f 100644 --- a/daml/splice-wallet-test/daml.yaml +++ b/daml/splice-wallet-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-wallet-test source: daml -version: 0.1.27 +version: 0.1.28 dependencies: - daml-prim - daml-stdlib @@ -29,6 +29,7 @@ data-dependencies: - ../../token-standard/splice-api-token-allocation-v2/.daml/dist/splice-api-token-allocation-v2-current.dar - ../../token-standard/splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml b/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml index d2a53d3f276..3cd85ce43b3 100644 --- a/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml +++ b/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml @@ -12,7 +12,7 @@ import DA.List.Total (head) import Splice.Amulet import Splice.AmuletRules import Splice.Api.RewardAssignmentV1 qualified as Api.RewardAssignmentV1 -import Splice.Testing.Utils (emptyExtraArgs) +import Splice.Testing.Utils (emptyExtraArgs, expectErrorId, expectMessageContains, submitMustFail'') import Splice.Round import Splice.Util import Splice.Wallet.MintingDelegation @@ -85,6 +85,7 @@ testMintingDelegation = do amount = developmentFundCouponAmount expiresAt = now `addRelTime` days 60 reason = "Test development fund coupon" + mintAfter = Some (now `addRelTime` minutes 1) let rewardCouponV2Amount = 42.0 rewardCouponV2Cid <- submit app.dso $ createCmd RewardCouponV2 with @@ -196,6 +197,67 @@ testMintingDelegation_checksExpiry = do pure () +testMintingDelegation_checksMintAfter : Script () +testMintingDelegation_checksMintAfter = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + + beneficiary <- allocateParty "beneficiary" + let delegate = aliceValidator.primaryParty + + now <- getTime + proposalCid <- submit beneficiary $ createCmd MintingDelegationProposal with + delegation = MintingDelegation with + dso = app.dso + beneficiary + delegate + expiresAt = now `addRelTime` days 30 + amuletMergeLimit = 10 + + MintingDelegationProposal_AcceptResult mintingDelegationCid <- submit delegate $ exerciseCmd proposalCid MintingDelegationProposal_Accept with + existingDelegationCid = None + + -- Advance rounds so the coupon can be minted + runNextIssuance app + runNextIssuance app + runNextIssuance app + + couponCreatedAt <- getTime + developmentFundCouponCid <- submit app.dso $ createCmd DevelopmentFundCoupon with + dso = app.dso + beneficiary + fundManager = app.dso + amount = 50.0 + expiresAt = couponCreatedAt `addRelTime` days 60 + reason = "Test development fund coupon" + mintAfter = Some (couponCreatedAt `addRelTime` minutes 2) + + (amuletRulesCid, _) <- getAmuletRules app + + -- Shared between the failing and the succeeding submission, so the only difference + -- between them is the elapsed time. + let submitOptions = actAs delegate <> readAs beneficiary <> readAs app.dso + mintCmd context = exerciseCmd mintingDelegationCid MintingDelegation_Mint with + inputs = [InputDevelopmentFundCoupon developmentFundCouponCid] + context = PaymentTransferContext with + context + amuletRules = amuletRulesCid + + -- Unhappy - mintAfter has not passed yet + contextBefore <- getTransferContext app alice None + submitMustFail'' + ( expectErrorId "stdlib.daml.com/deadline-not-exceeded" <> + expectMessageContains "deadline 'DevelopmentFundCoupon.mintAfter'" + ) + submitOptions + (mintCmd contextBefore) + + -- Happy - the same mint succeeds once mintAfter has passed + passTime (minutes 3) + contextAfter <- getTransferContext app alice None + _ <- submit submitOptions (mintCmd contextAfter) + + pure () + -- Archival of existing delegation succeeds only if the delegation parties match -- with the new delegation being proposed testMintingDelegation_AcceptWithExistingDelegationArchival : Script () diff --git a/daml/splice-wallet/daml.yaml b/daml/splice-wallet/daml.yaml index cc61e20a5c0..e07b423fd21 100644 --- a/daml/splice-wallet/daml.yaml +++ b/daml/splice-wallet/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-wallet source: daml -version: 0.1.23 +version: 0.1.24 dependencies: - daml-prim - daml-stdlib @@ -21,6 +21,7 @@ data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - -Wno-deprecated-exceptions diff --git a/daml/splice-wallet/daml/Splice/Wallet/BuyTrafficRequest.daml b/daml/splice-wallet/daml/Splice/Wallet/BuyTrafficRequest.daml index 8dba3b661c1..98d6012d39b 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/BuyTrafficRequest.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/BuyTrafficRequest.daml @@ -13,18 +13,21 @@ data BuyTrafficRequestTrackingInfo = BuyTrafficRequestTrackingInfo with trackingId : Text -- ^ used to deduplicate requests and query for the status endUserParty : Party -- ^ used in UserWalletTxLogParser's filterByParty - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data BuyTrafficRequest_CompleteResult = BuyTrafficRequest_CompleteResult with - purchasedTraffic : ContractId MemberTraffic - trackingInfo : BuyTrafficRequestTrackingInfo - senderChangeAmulet : Optional (ContractId Amulet) + purchasedTraffic : ContractId MemberTraffic + trackingInfo : BuyTrafficRequestTrackingInfo + senderChangeAmulet : Optional (ContractId Amulet) + deriving (Serializable) data BuyTrafficRequest_CancelResult = BuyTrafficRequest_CancelResult with - trackingInfo : BuyTrafficRequestTrackingInfo + trackingInfo : BuyTrafficRequestTrackingInfo + deriving (Serializable) data BuyTrafficRequest_ExpireResult = BuyTrafficRequest_ExpireResult with - trackingInfo : BuyTrafficRequestTrackingInfo + trackingInfo : BuyTrafficRequestTrackingInfo + deriving (Serializable) -- | A request by an end-user to the wallet's automation to buy traffic -- for a sequencer member diff --git a/daml/splice-wallet/daml/Splice/Wallet/Install.daml b/daml/splice-wallet/daml/Splice/Wallet/Install.daml index 6a64a9c4854..4e5b676029d 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/Install.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/Install.daml @@ -37,10 +37,11 @@ import Splice.Util data ExecutionContext = ExecutionContext with - dso : Party - endUser : Party - validator : Party - paymentContext : PaymentTransferContext + dso : Party + endUser : Party + validator : Party + paymentContext : PaymentTransferContext + deriving (Serializable) -- Main function for batched execution of multiple amulet operations. -- Given multiple amulet operations and transfer inputs, it executes the operations sequentially while @@ -283,7 +284,7 @@ data AmuletOperation providerFeaturedAppRightCid : Optional (ContractId FeaturedAppRight) amount : Decimal description : Optional Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Enum of the possible outcomes of operating on a amulet through the wallet. data AmuletOperationOutcome @@ -306,7 +307,7 @@ data AmuletOperationOutcome | COO_AcceptTransferPreapprovalProposal (ContractId TransferPreapproval) | COO_RenewTransferPreapproval (ContractId TransferPreapproval) | COO_TransferPreapprovalSend (Optional (ContractId Amulet)) - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data WalletAppInstall_ExecuteBatchResult = WalletAppInstall_ExecuteBatchResult with @@ -317,77 +318,95 @@ data WalletAppInstall_ExecuteBatchResult = WalletAppInstall_ExecuteBatchResult optEndUserParty : Optional Party -- The primary party on whose wallet the batch was executed. This is useful since the logic parsing -- the tx log entry currently cannot resolve the payload of the WalletAppInstall to get the party from there. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data WalletAppInstall_AppPaymentRequest_RejectResult = WalletAppInstall_AppPaymentRequest_RejectResult with terminatedAppPayment : ContractId TerminatedAppPayment + deriving (Serializable) data WalletAppInstall_AppPaymentRequest_ExpireResult = WalletAppInstall_AppPaymentRequest_ExpireResult with terminatedAppPayment : ContractId TerminatedAppPayment + deriving (Serializable) data WalletAppInstall_SubscriptionRequest_RejectResult = WalletAppInstall_SubscriptionRequest_RejectResult with terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data WalletAppInstall_SubscriptionIdleState_CancelSubscriptionResult = WalletAppInstall_SubscriptionIdleState_CancelSubscriptionResult with terminatedSubscription : ContractId TerminatedSubscription + deriving (Serializable) data WalletAppInstall_CreateTransferOfferResult = WalletAppInstall_CreateTransferOfferResult with transferOffer : ContractId TransferOffer + deriving (Serializable) data WalletAppInstall_TransferOffer_AcceptResult = WalletAppInstall_TransferOffer_AcceptResult with acceptedTransferOffer : ContractId AcceptedTransferOffer + deriving (Serializable) data WalletAppInstall_TransferOffer_RejectResult = WalletAppInstall_TransferOffer_RejectResult with trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data WalletAppInstall_TransferOffer_WithdrawResult = WalletAppInstall_TransferOffer_WithdrawResult with trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data WalletAppInstall_TransferOffer_ExpireResult = WalletAppInstall_TransferOffer_ExpireResult with trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data WalletAppInstall_AcceptedTransferOffer_AbortResult = WalletAppInstall_AcceptedTransferOffer_AbortResult with trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data WalletAppInstall_AcceptedTransferOffer_WithdrawResult = WalletAppInstall_AcceptedTransferOffer_WithdrawResult with trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data WalletAppInstall_AcceptedTransferOffer_ExpireResult = WalletAppInstall_AcceptedTransferOffer_ExpireResult with trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data WalletAppInstall_CreateBuyTrafficRequestResult = WalletAppInstall_CreateBuyTrafficRequestResult with buyTrafficRequest : ContractId BuyTrafficRequest + deriving (Serializable) data WalletAppInstall_BuyTrafficRequest_CancelResult = WalletAppInstall_BuyTrafficRequest_CancelResult with trackingInfo : BuyTrafficRequestTrackingInfo + deriving (Serializable) data WalletAppInstall_BuyTrafficRequest_ExpireResult = WalletAppInstall_BuyTrafficRequest_ExpireResult with trackingInfo : BuyTrafficRequestTrackingInfo + deriving (Serializable) data WalletAppInstall_FeaturedAppRights_CancelResult = WalletAppInstall_FeaturedAppRights_CancelResult + deriving (Serializable) data WalletAppInstall_FeaturedAppRights_SelfGrantResult = WalletAppInstall_FeaturedAppRights_SelfGrantResult with featuredAppRight : ContractId FeaturedAppRight + deriving (Serializable) data WalletAppInstall_TransferPreapprovalProposal_CreateResult = WalletAppInstall_TransferPreapprovalProposal_CreateResult with preapprovalProposalCid : ContractId TransferPreapprovalProposal + deriving (Serializable) template WalletAppInstall with diff --git a/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml b/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml index ce662ebf408..95759555362 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml @@ -128,30 +128,30 @@ template MintingDelegation data MintingDelegationProposal_AcceptResult = MintingDelegationProposal_AcceptResult with mintingDelegationCid : ContractId MintingDelegation - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data MintingDelegationProposal_RejectResult = MintingDelegationProposal_RejectResult {} - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data MintingDelegationProposal_WithdrawResult = MintingDelegationProposal_WithdrawResult {} - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data MintingDelegation_RejectResult = MintingDelegation_RejectResult {} - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data MintingDelegation_WithdrawResult = MintingDelegation_WithdrawResult {} - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data MintingDelegation_MintResult = MintingDelegation_MintResult with transferResult : TransferResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data MintingDelegation_AssignAndMintResult = MintingDelegation_AssignAndMintResult with assignResult : Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiariesResult transferResult : TransferResult - deriving (Show, Eq) + deriving (Show, Eq, Serializable) instance HasCheckedFetch MintingDelegation ForOwner where contractGroupId MintingDelegation{beneficiary, dso} = ForOwner with owner = beneficiary; dso diff --git a/daml/splice-wallet/daml/Splice/Wallet/TransferOffer.daml b/daml/splice-wallet/daml/Splice/Wallet/TransferOffer.daml index 46e90328419..e53a87650dc 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/TransferOffer.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/TransferOffer.daml @@ -16,25 +16,32 @@ import Splice.Util import Splice.Wallet.Payment data TransferOffer_AcceptResult = TransferOffer_AcceptResult with - acceptedTransferOffer : ContractId AcceptedTransferOffer + acceptedTransferOffer : ContractId AcceptedTransferOffer + deriving (Serializable) data TransferOffer_RejectResult = TransferOffer_RejectResult with - trackingInfo : TransferOfferTrackingInfo + trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data TransferOffer_WithdrawResult = TransferOffer_WithdrawResult with - trackingInfo : TransferOfferTrackingInfo + trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data TransferOffer_ExpireResult = TransferOffer_ExpireResult with - trackingInfo : TransferOfferTrackingInfo + trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data AcceptedTransferOffer_WithdrawResult = AcceptedTransferOffer_WithdrawResult with - trackingInfo : TransferOfferTrackingInfo + trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data AcceptedTransferOffer_AbortResult = AcceptedTransferOffer_AbortResult with - trackingInfo : TransferOfferTrackingInfo + trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) data AcceptedTransferOffer_ExpireResult = AcceptedTransferOffer_ExpireResult with - trackingInfo : TransferOfferTrackingInfo + trackingInfo : TransferOfferTrackingInfo + deriving (Serializable) template TransferOffer @@ -98,12 +105,13 @@ data TransferOfferTrackingInfo = TransferOfferTrackingInfo { trackingId : Text; -- used to deduplicate requests and query for the status sender : Party; -- used in UserWalletTxLogParser's filterByParty receiver : Party -- used in UserWalletTxLogParser's filterByParty -} deriving (Eq, Show) +} deriving (Eq, Show, Serializable) data AcceptedTransferOffer_CompleteResult = AcceptedTransferOffer_CompleteResult with - transferResult: TransferResult - trackingInfo : TransferOfferTrackingInfo - senderChangeAmulet : Optional (ContractId Amulet) + transferResult: TransferResult + trackingInfo : TransferOfferTrackingInfo + senderChangeAmulet : Optional (ContractId Amulet) + deriving (Serializable) diff --git a/daml/splice-wallet/daml/Splice/Wallet/TransferPreapproval.daml b/daml/splice-wallet/daml/Splice/Wallet/TransferPreapproval.daml index 3bc677de6cc..dcf27dedac1 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/TransferPreapproval.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/TransferPreapproval.daml @@ -38,6 +38,7 @@ template TransferPreapprovalProposal return TransferPreapprovalProposal_AcceptResult with .. data TransferPreapprovalProposal_AcceptResult = TransferPreapprovalProposal_AcceptResult with - transferPreapprovalCid: ContractId TransferPreapproval - transferResult: TransferResult - amuletPaid: Decimal + transferPreapprovalCid: ContractId TransferPreapproval + transferResult: TransferResult + amuletPaid: Decimal + deriving (Serializable) diff --git a/daml/splitwell-test/daml.yaml b/daml/splitwell-test/daml.yaml index 1294cd854af..a72ab763cab 100644 --- a/daml/splitwell-test/daml.yaml +++ b/daml/splitwell-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splitwell-test source: daml -version: 0.1.27 +version: 0.1.28 dependencies: - daml-prim - daml-stdlib @@ -15,6 +15,7 @@ data-dependencies: - ../splice-wallet/.daml/dist/splice-wallet-current.dar - ../splice-wallet-test/.daml/dist/splice-wallet-test-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splitwell/daml.yaml b/daml/splitwell/daml.yaml index c356123d961..72106633d71 100644 --- a/daml/splitwell/daml.yaml +++ b/daml/splitwell/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splitwell source: daml -version: 0.1.23 +version: 0.1.24 dependencies: - daml-prim - daml-stdlib @@ -10,6 +10,7 @@ data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar build-options: + - --explicit-serializable=yes - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches - --target=2.1 diff --git a/daml/splitwell/daml/Splice/Splitwell.daml b/daml/splitwell/daml/Splice/Splitwell.daml index 0dc1c207bf7..e32d9203216 100644 --- a/daml/splitwell/daml/Splice/Splitwell.daml +++ b/daml/splitwell/daml/Splice/Splitwell.daml @@ -23,22 +23,22 @@ data GroupKey = GroupKey owner : Party provider : Party id: GroupId - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Group id unique per owner newtype GroupId = GroupId Text - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data ForOwner = ForOwner with owner : Party provider : Party - deriving (Show, Eq) + deriving (Show, Eq, Serializable) data ForApp = ForApp with provider : Party - deriving (Show, Eq) + deriving (Show, Eq, Serializable) instance HasCheckedFetch Group ForApp where contractGroupId Group{..} = ForApp{..} @@ -343,7 +343,7 @@ data BalanceUpdateType | ExtBalanceUpdateType with dummyUnitField : () -- ^ Extension constructor (and field) to work around the current lack of upgrading for variants in Daml 3.0 - deriving (Eq, Show) + deriving (Eq, Show, Serializable) template GroupInvite with diff --git a/docs/src/release_notes_upcoming.rst b/docs/src/release_notes_upcoming.rst index 5507b4ddf87..a0c3bd996e2 100644 --- a/docs/src/release_notes_upcoming.rst +++ b/docs/src/release_notes_upcoming.rst @@ -7,11 +7,133 @@ release-notes:: Upcoming + - Postgres image in Compose, LocalNet & splice-postgres Helm chart + + - The default ``imageName`` of the postgres in ``splice-postgres`` has changed + from ``imageName: "postgres:14"`` to ``imageName: "postgres:14.24-trixie"``. + The image for Postgres 14 has been based on Debian Trixie since 14.18, released on 2025-05-08. + If you were somehow using an older or non-trixie version of the postgres image, + you might encounter `locale issues `_. + In that case you should use a compatible version of the image you're using. + Two related reminders: + + - Postgres 14 is reaching end-of-life in November 2026, so you should plan to upgrade to Postgres 18. + + - The ``splice-postgres`` chart is deprecated since Splice 0.6.13, so you should use something else. + - SV App - The deprecated (in 0.8.0) public ``/v0/dso`` endpoint has been removed. Use the public ``/v0/dso`` endpoint in the scan app if you need to fetch DSO info without SV operator credentials. + - Joining SVs now fetch DSO info during onboarding from a scan instance + (typically the sponsor's) instead of the sponsor SV app's deprecated public + ``/v0/dso`` endpoint. The scan is configured via the new ``.joinWithKeyOnboarding.sponsorScanUrl`` Helm value. + SVs who set the ``.joinWithKeyOnboarding`` key config must set it before upgrading. + - Helm - The deprecated `splice-domain` Helm chart has been removed. + + - Scan App + + - *Breaking* Scan will no longer return featured app rights as part of the choice context for CC transfers and allocations + once ``no-featured-app-choice-context`` has been set in ``svOperationsSwitchOverTimes`` and the switchover time has been reached. + This change was made to avoid apps accidentally not complying with the marker guidance set by the Canton foundation which requires precise control over the markers issued. If you + really do intend to feature a CC transfer, you can query for the contract through ``/api/scan/v0/featured-apps/{provider_party_id}`` on Scan + and add it to the choice context under the ``featured-app-right`` choice context key. + + Validators *must* upgrade to 0.8.0 before the SVs vote on + setting the ``svOperationsSwitchOverTimes`` field or risk + breakage as the ``DsoRules`` contract cannot be downgraded + once the field is set. + + + - CometBFT + + - Increased default resources of watchdog and made it only query for metrics it needs. + + - Daml + + - .. warning:: + + **Action required for app devs:** apps with Daml code that statically depends on + ``splice-amulet`` should recompile against the new version. When the SVs set + ``minDevelopmentFundMintingDelay`` or ``developmentFundManagerBlacklist``, those + values block downgrades to package versions that do not enforce them. From that + point, code that still links against the old ``AmuletConfig`` stops working. + + - Add an optional ``mintAfter`` field to ``DevelopmentFundCoupon``. Add + ``minDevelopmentFundMintingDelay`` and ``developmentFundManagerBlacklist`` to + ``AmuletConfig``. + The SVs need to set a minimum delay between coupon allocation and minting + by the beneficiary. They can also block minting of coupons from blacklisted + fund managers. + + This change addresses suggestion QS2 from Quantstamp in the + `Canton Coin 2026 audit `_. + + This release does not change existing behavior. Both config fields default to unset. + The SVs enforce the minting delay when they set ``minDevelopmentFundMintingDelay`` + to a non-zero value. They block minting of the coupons of a development fund manager + when they add that manager to ``developmentFundManagerBlacklist``. While + ``minDevelopmentFundMintingDelay`` stays unset, callers can allocate coupons + without ``mintAfter`` and mint them immediately, as before. + + Callers of ``AmuletRules_AllocateDevelopmentFundCoupon`` must change their call sites + before the SVs set a non-zero delay. After that, the choice rejects allocations + that omit ``mintAfter``. Coupons allocated before that vote have no ``mintAfter``. + A beneficiary can mint those coupons with no delay. + + - Wallet UI + + - The development fund allocation form now has a ``Mint After`` field, which sets the + earliest time at which the beneficiary can mint the coupon. While + the ``minDevelopmentFundMintingDelay`` stays unset in the ``AmuletConfig``, the field is + optional and the coupons are allocated without a mint-after + constraint. + + - The development fund coupon list now shows a ``Mint After`` column. + + - Validator + + - *breaking*: The deprecated ``TransferCommand`` functionality + enabled by ``canton.validator-apps.validator_backend.enable-deprecated-transfer-command-support=true`` + has been fully removed. Migrate to token standard transfers + and remove the flag. + + - The wallet endpoint ``/v0/wallet/development-fund-coupons/allocate`` accepts an + optional ``mintAfter`` field, in epoch microseconds. Callers must set it once the SVs + configure a non-zero ``minDevelopmentFundMintingDelay``, otherwise the allocations + will be rejected. + + - Helm + + - The node pods of the operator charts now accept a ``priorityClassName``, so operators can + protect a node from eviction under resource pressure. It is unset by default, which leaves + scheduling behaviour unchanged. + + - The Postgres role and bootstrap database used by the validator and participant charts are + no longer hardcoded. ``persistence.user`` and ``persistence.bootstrapDatabaseName`` are now + honoured by the ``pg-init`` and wait containers, by the Postgres exporter sidecar, and — for + the participant — by the Canton node itself, which previously always connected as + ``cnadmin`` regardless of the configured value. They default to ``cnadmin`` and + ``cantonnet``, so rendered output is unchanged unless you set them. This matters for + operators moving off ``splice-postgres`` to a self-provisioned or managed Postgres, where a + ``cnadmin`` superuser and a ``cantonnet`` database may not be available to create. The SV + charts are not covered yet and still use the hardcoded values. + + - All Splice Helm charts now set ``automountServiceAccountToken: false`` on the pods they + deploy. Splice components do not use the Kubernetes API, so pods no longer receive an + API-server credential by default; this reduces the impact of a compromised pod in + clusters where permissions are bound to the namespace's ``default`` service account. + If your deployment relies on the mounted token, for example through a custom service + account set via ``serviceAccountName``, you can restore the previous behavior by setting + the new ``automountServiceAccountToken`` Helm value to ``true``. + + - The (long unused) `splice-domain` Helm chart is deprecated and will be removed in 0.9.0. + + - SV UI + + - The ``AmuletRules_SetConfig`` proposal form can now set + ``minDevelopmentFundMintingDelay`` and ``developmentFundManagerBlacklist``. diff --git a/project/BuildCommon.scala b/project/BuildCommon.scala index 4e04762d8c3..0c31d913a29 100644 --- a/project/BuildCommon.scala +++ b/project/BuildCommon.scala @@ -571,7 +571,9 @@ object BuildCommon { BuildInfoKey("version", version), // hacked. scalaVersion, sbtVersion, - BuildInfoKey("damlLibrariesVersion" -> CantonDependencies.daml_libraries_version), + // We don't actually care about the damlLibrariesVersion but some of the Canton code needs it to compile. + // We just set it to the canton version which isn't right but seems less annoying than having to maintain the actual version. + BuildInfoKey("damlLibrariesVersion" -> CantonDependencies.canton_library_version), BuildInfoKey("stableProtocolVersions" -> List("34", "35")), BuildInfoKey("betaProtocolVersions" -> List()), ), diff --git a/project/CantonDependencies.scala b/project/CantonDependencies.scala index 93e5c65bd79..eb8e9c91a4b 100644 --- a/project/CantonDependencies.scala +++ b/project/CantonDependencies.scala @@ -6,10 +6,10 @@ import sbt.* /** Copied from Canton OSS repo. */ object CantonDependencies { // Slightly changed compared to Canton OSS repo to avoid the need for a meta sbt project - val version: String = "3.5.0-snapshot.20260401.14638.0.v9a1531c5" - val canton_library_version = "3.5.7-snapshot.20260630.19042.0.vc85c6a30" + val canton_library_version = "3.5.15" + // Canton does some sbt magic that depends on this field called version even though it is not explicitly used. + val version = canton_library_version val daml_language_versions = Seq("2.1") - val daml_libraries_version = version // Defined in `../nix/dpm-sdk-sources.json`, as the compiler version is also used by // the non-sbt based docker build. val daml_compiler_version = sys.env("DAML_COMPILER_VERSION") diff --git a/project/ignore-patterns/canton_network_test_log.ignore.txt b/project/ignore-patterns/canton_network_test_log.ignore.txt index 63edef0dbe2..3213ff7cc29 100644 --- a/project/ignore-patterns/canton_network_test_log.ignore.txt +++ b/project/ignore-patterns/canton_network_test_log.ignore.txt @@ -88,6 +88,15 @@ ForkJoinIdlenessExecutorService.*shutdown did not complete gracefully in allotte Consensus not reached.*BftScanConnection:BftScanConnectionTest.* Consensus not reached.*BftScanConnection:ConfigurationProvidedBftScanConnectionIntegrationTest.* +# TODO (#6502): under TBAR the reward-accounting BFT reads emit two related +# WARNs during bootstrap — "Consensus not reached." when the retry loop +# gives up (no peer can converge to Ok) and "The following Scan URLs +# disagreed with consensus" when a minority disagrees with the reached +# consensus. Both are scoped to the two triggers that call these reads so +# real BFT problems elsewhere still surface. Removed once #6392's two-phase +# probe lands. +Consensus not reached.*BftScanConnection:.*"span-name":"(SummarizingMiningRoundTrigger|CalculateRewardsTrigger)" +The following Scan URLs disagreed with consensus.*BftScanConnection:.*"span-name":"(SummarizingMiningRoundTrigger|CalculateRewardsTrigger)" # internal selenium error. Potentially intermittent Exception managing firefox: error sending request for url .* http2 error: stream error received: refused stream before processing any application @@ -134,7 +143,7 @@ Previous channel ManagedChannelImpl.* was garbage collected without being shut d # This is logged by pekko's RestartSource. Ideally we'd prevent it from logging that when shutting down, # but it doesn't offer a clean way of doing that. Lowering log level also doesn't seem sensible, # as we don't want to miss non-shutdown warnings in production. -Restarting stream due to failure.*Channel shutdown invoked +Restarting stream due to failure.*Channel shutdown(Now)? invoked # TODO(#564) - check what's happening here during shutdown Timeout 10 seconds expired, but readers are still active. Shutting down forcibly.*LsuIntegrationTest diff --git a/scripts/check-multiarch-images.py b/scripts/check-multiarch-images.py new file mode 100755 index 00000000000..2215e4128ca --- /dev/null +++ b/scripts/check-multiarch-images.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Fail if any digest-pinned container image is not a multi-arch digest.""" + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from dockerfile_parse import DockerfileParser + +# Matches concrete image references that pin a digest, e.g. ubuntu:24.04@sha256: +# This intentionally does not match references that use variable digests +IMAGE_REF_RE = re.compile( + r"([a-zA-Z0-9][a-zA-Z0-9._/-]*):([^@\s]+)@sha256:([a-f0-9]{64})" +) + +# Some Dockerfiles reuse the ARG name `image_sha256` with a different value for each image +IMAGE_SHA256_MAP = { + "cluster/images/canton/Dockerfile": "CANTON_BASE_IMAGE_SHA256", + "cluster/images/canton-mediator/Dockerfile": "CANTON_MEDIATOR_IMAGE_SHA256", + "cluster/images/canton-participant/Dockerfile": "CANTON_PARTICIPANT_IMAGE_SHA256", + "cluster/images/canton-sequencer/Dockerfile": "CANTON_SEQUENCER_IMAGE_SHA256", +} + +# List of file-path regexes. Any digest-pinned image coming from a matching file is skipped by the check +IGNORED_FILE_PATTERNS = [ + re.compile(r"^cluster/images/cometbft/Dockerfile$"), + re.compile(r"^cluster/images/splice-test-cometbft/Dockerfile$"), +] + + +def main() -> int: + _setup_env() + + # Collect all digest-pinned references and keep the first file they were seen in. + # The set semantics of the dict keys give us deduplication for free. + refs_by_file: dict[tuple[str, str, str], str] = {} + + # Dockerfiles may use variable digests (e.g. ARG $cometbft_sha), so parse all of them. + for file in _tracked_dockerfiles(): + path = Path(file) + if not path.is_file() or _file_is_ignored(file): + continue + for ref in _refs_from_dockerfile(path): + refs_by_file.setdefault(ref, file) + + # Other files only need to be inspected if they contain a concrete digest reference. + for file in _tracked_files_with_digest_refs(): + path = Path(file) + if not path.is_file() or _file_is_ignored(file): + continue + for ref in _refs_from_plain_file(path): + refs_by_file.setdefault(ref, file) + + failures = 0 + for (image, tag, digest), file in refs_by_file.items(): + ref = f"{image}:{tag}@sha256:{digest}" + print(f"Inspecting {ref} (from {file})") + if _inspect(image, digest): + print(f" OK: {ref} is multi-arch") + else: + print(f"ERROR: {ref} (from {file}) is not pinned to a multi-arch digest") + failures += 1 + + if failures > 0: + print(f"FAIL: {failures} image(s) are not pinned to multi-arch digests") + return 1 + + print("OK: all pinned images are multi-arch") + return 0 + + +def _setup_env() -> None: + """Export lowercase aliases for Dockerfile ARG references""" + os.environ.setdefault("canton_version", os.environ.get("CANTON_VERSION", "")) + os.environ.setdefault( + "cometbft_version", os.environ.get("COMETBFT_RELEASE_VERSION", "") + ) + os.environ.setdefault("cometbft_sha", os.environ.get("COMETBFT_IMAGE_SHA256", "")) + + +def _image_sha256_for(file: str) -> str | None: + key = IMAGE_SHA256_MAP.get(file) + if not key: + return None + return os.environ.get(key) + + +def _inspect(image: str, digest: str) -> bool: + """Return True if the digest resolves to a multi-arch manifest list/index. + + Uses `skopeo inspect --raw`. A multi-arch digest has a `.manifests` array + with at least one entry; a single-arch digest has no `.manifests`. + """ + # skopeo does not accept both a tag and a digest, so drop the tag. + ref = f"docker://{image}@sha256:{digest}" + try: + raw = subprocess.run( + ["skopeo", "inspect", "--raw", "--no-creds", ref], + check=True, + capture_output=True, + text=True, + ).stdout + except subprocess.CalledProcessError as e: + print(f"ERROR: unable to inspect {ref}: {e.stderr.strip() or e}", file=sys.stderr) + return False + + try: + manifest = json.loads(raw) + except json.JSONDecodeError as e: + print(f"ERROR: invalid manifest JSON for {ref}: {e}", file=sys.stderr) + return False + + manifests = manifest.get("manifests") + return isinstance(manifests, list) and len(manifests) > 0 + + +def _extract_refs_from_text(text: str) -> set[tuple[str, str, str]]: + """Extract (image_name, tag, digest) tuples from arbitrary text.""" + return set(IMAGE_REF_RE.findall(text)) + + +def _refs_from_dockerfile(path: Path) -> set[tuple[str, str, str]]: + """Parse a Dockerfile and resolve any variable references in FROM images.""" + # Set image_sha256 for this specific Dockerfile before resolving variables + image_sha256 = _image_sha256_for(str(path)) + if image_sha256: + os.environ["image_sha256"] = image_sha256 + elif "image_sha256" in os.environ: + del os.environ["image_sha256"] + + refs: set[tuple[str, str, str]] = set() + parser = DockerfileParser(str(path)) + for instruction in parser.structure: + if instruction["instruction"] != "FROM": + continue + value = instruction["value"] + # Drop stage aliases: "image:tag AS stage" + value = re.sub(r"\s+AS\s+\S+$", "", value, flags=re.IGNORECASE) + # Resolve $var / ${var} references using environment variables + resolved = os.path.expandvars(value) + refs.update(_extract_refs_from_text(resolved)) + return refs + + +def _refs_from_plain_file(path: Path) -> set[tuple[str, str, str]]: + """Scan a non-Dockerfile for digest-pinned image references.""" + refs: set[tuple[str, str, str]] = set() + with open(path, encoding="utf-8", errors="ignore") as f: + for line in f: + if line.lstrip().startswith("#"): + continue + refs.update(_extract_refs_from_text(line)) + return refs + + +def _tracked_dockerfiles() -> list[str]: + result = subprocess.run( + ["git", "ls-files", "--", "**/Dockerfile"], + check=True, + capture_output=True, + text=True, + ) + return result.stdout.splitlines() + + +def _tracked_files_with_digest_refs() -> list[str]: + result = subprocess.run( + ["git", "grep", "-l", "-E", r"@sha256:[a-f0-9]{64}", "--", ":!**/Dockerfile"], + check=False, + capture_output=True, + text=True, + ) + return result.stdout.splitlines() + + +def _file_is_ignored(file: str) -> bool: + for pattern in IGNORED_FILE_PATTERNS: + if pattern.search(file): + return True + return False + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/collect-node-configs.sh b/scripts/collect-node-configs.sh new file mode 100755 index 00000000000..3d4cf03caab --- /dev/null +++ b/scripts/collect-node-configs.sh @@ -0,0 +1,242 @@ +#!/usr/bin/env bash + +# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Collect the fully resolved HOCON configs of all Canton and Splice nodes in a +# Kubernetes namespace. +# +# Every Canton node (participant, sequencer, mediator) and every Splice app +# (sv-app, validator-app, scan-app, ...) logs its complete, resolved +# configuration exactly once, right after startup. Secrets in that dump are +# already redacted by the apps themselves. This script fetches those log lines +# with `kubectl logs`, extracts the config from them and writes one `.conf` +# file per node. +# +# If a node has been running for a long time, the config line may have rotated +# out of the Kubernetes log buffer. In that case the script offers to restart +# the node's Deployment (once) so that the config gets logged again. This +# causes a short downtime of that node. The restart is done via +# `kubectl rollout restart`. +# +# Requirements: bash, kubectl (configured for the target cluster), jq, grep, +# sed, and `zip` if --zip is used. +# +# Usage: +# collect-node-configs.sh NAMESPACE [--out DIR] [--zip] [--yes] +# +# --out DIR Directory to write the config files to. +# Default: ./node-configs-NAMESPACE- +# --zip Additionally create DIR.zip with all collected configs. +# --yes Do not ask before restarting a node whose logs no longer +# contain the config; restart it right away. + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +usage() { + sed -n '/^# Usage:/,/^$/p' "$0" | sed 's/^# \{0,1\}//' + exit 1 +} + +if [[ $# -lt 1 ]]; then usage; fi +namespace="$1" +shift + +out_dir="" +make_zip=false +assume_yes=false +while [[ $# -gt 0 ]]; do + case "$1" in + --out) out_dir="$2"; shift 2 ;; + --zip) make_zip=true; shift ;; + --yes) assume_yes=true; shift ;; + *) echo "Unknown argument: $1" >&2; usage ;; + esac +done + +if [[ -z "$out_dir" ]]; then + out_dir="./node-configs-${namespace}-$(date -u +%Y%m%dT%H%M%SZ)" +fi + +for tool in kubectl jq grep sed; do + command -v "$tool" >/dev/null || { echo "Required tool not found: $tool" >&2; exit 1; } +done +if $make_zip; then + command -v zip >/dev/null || { echo "Required tool not found: zip (needed for --zip)" >&2; exit 1; } +fi + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# The container images of the nodes we collect configs from. Everything else in +# the namespace (postgres, cometbft, web UIs, ...) is skipped. +node_images=( + canton-participant + canton-sequencer + canton-cometbft-sequencer + canton-mediator + sv-app + validator-app + scan-app + splitwell-app +) + +# The markers with which the two kinds of nodes start their config log message. +canton_marker="Starting up with resolved config" +splice_marker="SpliceEnvironment with config = {" + +# extract_config DEPLOYMENT CONTAINER +# +# Prints the resolved config found in the logs of the given container of the +# deployment's pod, or nothing (and returns non-zero) if the logs do not +# contain it. +# +# Log lines are JSON objects with the config embedded in the "message" field +# (with "\n" escapes). We grep for the marker first so that jq only ever sees +# the relevant lines, take the most recent one (in case the container +# restarted within the retained log window), unescape it with jq, and finally +# strip the marker line itself (and, for Splice apps, the closing "}" that +# wraps the config) so that only the HOCON config remains. +extract_config() { + local deployment="$1" container="$2" + local line + line="$(kubectl logs -n "$namespace" "deployment/$deployment" -c "$container" --tail=-1 \ + | grep -F -e "\"message\":\"${canton_marker}" -e "\"message\":\"${splice_marker}" \ + | tail -n 1 || true)" + if [[ -z "$line" ]]; then + return 1 + fi + if [[ "$line" == *"\"message\":\"${splice_marker}"* ]]; then + # Splice: drop the first line (marker) and the last line (closing brace). + printf '%s\n' "$line" | jq -r '.message' | sed '1d;$d' + else + # Canton: drop the first line (marker). + printf '%s\n' "$line" | jq -r '.message' | sed '1d' + fi +} + +# restart_and_extract DEPLOYMENT CONTAINER +# +# Restarts the deployment, waits for the new pod to log its config, and prints +# that config. Returns non-zero on timeout. +# +# Note that `kubectl rollout restart` records the restart time as an +# annotation on the deployment's pod template. Helm/Pulumi will overwrite that +# annotation again on the next deploy; it has no other effect. +restart_and_extract() { + local deployment="$1" container="$2" + local timeout_seconds=300 interval_seconds=5 waited=0 + local config + + echo " Restarting deployment $deployment ..." >&2 + kubectl rollout restart -n "$namespace" "deployment/$deployment" >/dev/null + + while [[ $waited -lt $timeout_seconds ]]; do + sleep "$interval_seconds" + waited=$((waited + interval_seconds)) + # While the old pod is still shutting down, or before the new pod exists, + # this either fails or finds no config; we just keep polling. + if config="$(extract_config "$deployment" "$container" 2>/dev/null)"; then + echo " New pod logged its config after ${waited}s." >&2 + printf '%s\n' "$config" + return 0 + fi + done + echo " Timed out after ${timeout_seconds}s waiting for $deployment to log its config." >&2 + return 1 +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +mkdir -p "$out_dir" +echo "Collecting node configs from namespace '$namespace' into '$out_dir'" + +# List all deployments with their containers, as lines of +# " ". +all_containers="$(kubectl get deployments -n "$namespace" -o json | jq -r ' + .items[] + | .metadata.name as $deployment + | .spec.template.spec.containers[] + | [$deployment, .name, (.image | split("/") | last | split("@")[0] | split(":")[0])] + | @tsv +')" + +# Keep only the node containers (see node_images above). +nodes="" +skipped=() +while IFS=$'\t' read -r deployment container image; do + [[ -z "$deployment" ]] && continue + if printf '%s\n' "${node_images[@]}" | grep -q -x -F "$image"; then + nodes+="${deployment} ${container}"$'\n' + else + skipped+=("$deployment ($image)") + fi +done <<<"$all_containers" + +if [[ ${#skipped[@]} -gt 0 ]]; then + echo "Skipping deployments that are not Canton or Splice nodes: ${skipped[*]}" +fi +if [[ -z "$nodes" ]]; then + echo "No Canton or Splice node deployments found in namespace '$namespace'." >&2 + exit 1 +fi + +collected=() +failed=() + +# The node list is read from file descriptor 3 so that stdin stays available +# for the interactive confirmation prompt below. +while IFS=$'\t' read -r -u 3 deployment container; do + [[ -z "$deployment" ]] && continue + echo "* $deployment (container $container)" + target="$out_dir/$deployment.conf" + + if config="$(extract_config "$deployment" "$container")"; then + printf '%s\n' "$config" > "$target" + echo " Saved to $target" + collected+=("$deployment") + continue + fi + + echo " Logs of $deployment no longer contain the startup config." + if ! $assume_yes; then + read -r -p " Restart the deployment so that the config gets logged again? This causes a short downtime. [y/N] " answer + if [[ "$answer" != [yY] ]]; then + echo " Skipping $deployment." + failed+=("$deployment") + continue + fi + fi + + if config="$(restart_and_extract "$deployment" "$container")"; then + printf '%s\n' "$config" > "$target" + echo " Saved to $target" + collected+=("$deployment") + else + failed+=("$deployment") + fi +done 3<<<"$nodes" + +echo +echo "Collected ${#collected[@]} config(s) in '$out_dir': ${collected[*]:-}" +if [[ ${#failed[@]} -gt 0 ]]; then + echo "Failed to collect: ${failed[*]}" >&2 +fi + +if $make_zip; then + zip_file="${out_dir%/}.zip" + rm -f "$zip_file" + (cd "$(dirname "$out_dir")" && zip -q -r "$(basename "$zip_file")" "$(basename "$out_dir")") + echo "Archive written to $zip_file" +fi + +if [[ ${#failed[@]} -gt 0 ]]; then + exit 2 +fi diff --git a/scripts/external-signing/external-signing.py b/scripts/external-signing/external-signing.py index db1be6a9278..f26b4fcd32c 100755 --- a/scripts/external-signing/external-signing.py +++ b/scripts/external-signing/external-signing.py @@ -379,31 +379,6 @@ def parse_cli_args(): parser_setup_transfer_preapproval.add_argument("--key-directory", required=True) parser_setup_transfer_preapproval.add_argument("--key-name", required=True) - parser_transfer_preapproval_send = subparsers.add_parser( - "transfer-preapproval-send", - help="Initiate a pre-approved transfer", - ) - parser_transfer_preapproval_send.set_defaults( - handler=handle_transfer_preapproval_send - ) - parser_transfer_preapproval_send.add_argument( - "--validator-url", - help="Address of Validator API", - required=True - ) - - parser_transfer_preapproval_send.add_argument( - "--scan-url", - help="Address of Scan API", - required=True - ) - parser_transfer_preapproval_send.add_argument("--sender-party-id", required=True) - parser_transfer_preapproval_send.add_argument("--receiver-party-id", required=True) - parser_transfer_preapproval_send.add_argument("--amount", required=True) - parser_transfer_preapproval_send.add_argument("--nonce", required=True) - parser_transfer_preapproval_send.add_argument("--key-directory", required=True) - parser_transfer_preapproval_send.add_argument("--key-name", required=True) - return parser.parse_args() @@ -419,10 +394,6 @@ async def main(): async with aiohttp.ClientSession(headers=headers) as session: if args.subcommand == "generate-key-pair": await args.handler(args) - elif args.subcommand == 'transfer-preapproval-send': - validator_client = ValidatorClient(session, args.validator_url) - scan_client = ScanClient(session, args.scan_url) - await args.handler(args, validator_client, scan_client) else: validator_client = ValidatorClient(session, args.validator_url) await args.handler(args, validator_client) diff --git a/scripts/postgres.sh b/scripts/postgres.sh index ab62eb4dcbc..20456bdb693 100755 --- a/scripts/postgres.sh +++ b/scripts/postgres.sh @@ -5,7 +5,7 @@ set -eou pipefail -DOCKER_POSTGRES_IMAGE_NAME="postgres:18" +DOCKER_POSTGRES_IMAGE_NAME="postgres:18-trixie" # Postgres settings DOCKER_POSTGRES_CONTAINER_NAME="postgres-for-splice-node" diff --git a/test-full-class-names-non-integration.log b/test-full-class-names-non-integration.log index a52b083e472..de9cb5d130c 100644 --- a/test-full-class-names-non-integration.log +++ b/test-full-class-names-non-integration.log @@ -60,6 +60,7 @@ org.lfdecentralizedtrust.splice.sv.util.WeightDistributionForSvTest org.lfdecentralizedtrust.splice.unit.http.HttpClientProxyTest org.lfdecentralizedtrust.splice.unit.store.db.AcsJdbcTypesTest org.lfdecentralizedtrust.splice.util.AmuletConfigScheduleTest +org.lfdecentralizedtrust.splice.util.IpCidrRateLimitsTest org.lfdecentralizedtrust.splice.util.PolicyTest org.lfdecentralizedtrust.splice.util.SpliceCircuitBreakerTest org.lfdecentralizedtrust.splice.util.SpliceRateLimiterTest diff --git a/test-full-class-names-sim-time.log b/test-full-class-names-sim-time.log index 6007a6c4294..e202a489767 100644 --- a/test-full-class-names-sim-time.log +++ b/test-full-class-names-sim-time.log @@ -1,5 +1,6 @@ org.lfdecentralizedtrust.splice.integration.tests.DisabledWalletTimeBasedIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ExternallySignedTxsTimeBasedIntegrationTest +org.lfdecentralizedtrust.splice.integration.tests.FeaturedAppRightSwitchOverTimeBasedIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.FollowAmuletConversionRateFeedTimeBasedIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ScanWithGradualStartsTimeBasedIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.SvExpiredRewardsCollectionTimeBasedIntegrationTest diff --git a/test-full-class-names.log b/test-full-class-names.log index 38f35e559a9..3ae8af41a97 100644 --- a/test-full-class-names.log +++ b/test-full-class-names.log @@ -27,7 +27,6 @@ org.lfdecentralizedtrust.splice.integration.tests.FeaturedAppActivityMarkerInteg org.lfdecentralizedtrust.splice.integration.tests.GcpBucketPeriodicBackupIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.MemberTrafficIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.MultiHostValidatorOperatorIntegrationTest -org.lfdecentralizedtrust.splice.integration.tests.PackageWithDependencyIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ParticipantKmsIdentitiesIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.ParticipantPlaintextIdentitiesIntegrationTest org.lfdecentralizedtrust.splice.integration.tests.PeriodicTopologySnapshotIntegrationTest diff --git a/token-standard/examples/splice-test-token-v1-test/daml.yaml b/token-standard/examples/splice-test-token-v1-test/daml.yaml index 59b620cd0e1..bb60e40661a 100644 --- a/token-standard/examples/splice-test-token-v1-test/daml.yaml +++ b/token-standard/examples/splice-test-token-v1-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-test-token-v1-test source: daml -version: 1.0.1 +version: 1.0.2 dependencies: - daml-prim - daml-stdlib @@ -23,6 +23,7 @@ data-dependencies: - ../../splice-token-standard-v2-test/.daml/dist/splice-token-standard-v2-test-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/examples/splice-test-token-v1/daml.yaml b/token-standard/examples/splice-test-token-v1/daml.yaml index c32e8c4d2be..525af68e915 100644 --- a/token-standard/examples/splice-test-token-v1/daml.yaml +++ b/token-standard/examples/splice-test-token-v1/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-test-token-v1 source: daml -version: 1.0.0 +version: 1.0.1 dependencies: - daml-prim - daml-stdlib @@ -14,6 +14,7 @@ data-dependencies: - ../../splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/examples/splice-test-token-v2-test/daml.yaml b/token-standard/examples/splice-test-token-v2-test/daml.yaml index c7a9b6fdf19..fa795239a92 100644 --- a/token-standard/examples/splice-test-token-v2-test/daml.yaml +++ b/token-standard/examples/splice-test-token-v2-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-test-token-v2-test source: daml -version: 1.0.1 +version: 1.0.2 dependencies: - daml-prim - daml-stdlib @@ -28,6 +28,7 @@ data-dependencies: - ../../splice-token-standard-v2-test/.daml/dist/splice-token-standard-v2-test-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/examples/splice-test-token-v2-test/daml/Splice/Testing/Tokens/TestTokenV2/TestTransfer_V1V2Mixed.daml b/token-standard/examples/splice-test-token-v2-test/daml/Splice/Testing/Tokens/TestTokenV2/TestTransfer_V1V2Mixed.daml index 452b2017190..26431a1d7da 100644 --- a/token-standard/examples/splice-test-token-v2-test/daml/Splice/Testing/Tokens/TestTokenV2/TestTransfer_V1V2Mixed.daml +++ b/token-standard/examples/splice-test-token-v2-test/daml/Splice/Testing/Tokens/TestTokenV2/TestTransfer_V1V2Mixed.daml @@ -43,7 +43,7 @@ data TestCase = TestCase with bobAccount : V2.Account bobAccountConfig : Optional TestTokenV2.AccountConfig aliceToBobMeta : Metadata - deriving (Eq, Show) + deriving (Eq, Show, Serializable) mkBasicAccountTestCase : Party -> Script TestCase mkBasicAccountTestCase _admin = do diff --git a/token-standard/examples/splice-test-token-v2/daml.yaml b/token-standard/examples/splice-test-token-v2/daml.yaml index cbf2d840d9f..e65691f5518 100644 --- a/token-standard/examples/splice-test-token-v2/daml.yaml +++ b/token-standard/examples/splice-test-token-v2/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.5.2 name: splice-test-token-v2 source: daml -version: 1.0.0 +version: 1.0.1 dependencies: - daml-prim - daml-stdlib @@ -19,6 +19,7 @@ data-dependencies: - ../../splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2.daml b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2.daml index 8dc40d5face..ace14b6174a 100644 --- a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2.daml +++ b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2.daml @@ -160,7 +160,7 @@ template TokenRules with data TokenRules_OfferMintResult = TokenRules_OfferMintResult with offerCid : ContractId TokenTransferOffer meta : Metadata - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Create a V2 transfer offer. -- diff --git a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/AccountConfig.daml b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/AccountConfig.daml index 7e6ce123c07..bd5f1fa99a7 100644 --- a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/AccountConfig.daml +++ b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/AccountConfig.daml @@ -67,7 +67,7 @@ data PartyConfig = PartyConfig with -- ^ Whether the party can initiate token standard actions on the account. mustApprove : Bool -- ^ Whether the party must approve token standard actions on the account for them to be executed. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A configuration of the authorization rules for an account. -- @@ -161,13 +161,13 @@ template AccountConfig data AccountProposal_Accept_Result = AccountProposal_Accept_Result with accountConfig : ContractId AccountConfig - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AccountProposal_Reject_Result = AccountProposal_Reject_Result with - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data AccountProposal_Withdraw_Result = AccountProposal_Withdraw_Result with - deriving (Eq, Show) + deriving (Eq, Show, Serializable) template AccountProposal with @@ -206,7 +206,7 @@ data TransferInstructionState | TIS_Withdrawn | TIS_Rejected | TIS_Accepted - deriving (Eq, Ord, Enum, Show) + deriving (Eq, Ord, Enum, Show, Serializable) transferInstructionStateMachineSpec : V2.Account -> V2.Account -> AuthStateMachine V2.TransferInstructionAction TransferInstructionState @@ -227,7 +227,7 @@ data AllocationInstructionState = AIS_Init | AIS_Withdrawn | AIS_Accepted - deriving (Eq, Ord, Enum, Show) + deriving (Eq, Ord, Enum, Show, Serializable) allocationInstructionStateMachineSpec : V2.Account -> AuthStateMachine V2.AllocationInstructionAction AllocationInstructionState @@ -246,7 +246,7 @@ data AllocationState | AS_Withdrawn | AS_Cancelled | AS_Settled - deriving (Eq, Ord, Enum, Show) + deriving (Eq, Ord, Enum, Show, Serializable) allocationStateMachineSpec : V2.Account -> [Party] -> Party -> AuthStateMachine V2.AllocationAction AllocationState @@ -389,7 +389,7 @@ data AuthSpec actionType -- ^ The authorizers of another action need to authorize | STAS_Parties [Party] -- ^ A specific set of parties needs to authorize - deriving (Show) + deriving (Show, Serializable) -- The specification of an authorization state machine -- The outer map is the current state diff --git a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Allocation.daml b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Allocation.daml index d64fc201268..0c537ca2221 100644 --- a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Allocation.daml +++ b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Allocation.daml @@ -538,6 +538,7 @@ token_allocation_settleImplV2 callSource allocationKind this self arg = do -- | Which kind of allocation to create. data AllocationKind = CreateV1Allocation | CreateV2Allocation + deriving (Serializable) createTokenAllocation : AllocationKind -> TokenAllocationV2 -> Update (ContractId V2.Allocation) createTokenAllocation allocationKind allocationV2 = case allocationKind of diff --git a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Util.daml b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Util.daml index 01f4c073d0c..3853df83647 100644 --- a/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Util.daml +++ b/token-standard/examples/splice-test-token-v2/daml/Splice/Testing/Tokens/TestTokenV2/Util.daml @@ -51,6 +51,7 @@ getEventLogFromContext context = coerceInterfaceContractId <$> getTokenRulesFrom ---------------------- data CallSource = CalledFromV1 | CalledFromV2 + deriving (Serializable) archiveIfCalledFromV2 : HasArchive t => CallSource -> Optional (ContractId t) -> Update () archiveIfCalledFromV2 CalledFromV2 (Some cid) = archive cid diff --git a/token-standard/examples/splice-token-test-dummy-holding/daml.yaml b/token-standard/examples/splice-token-test-dummy-holding/daml.yaml index aa0f9b743bf..1bc6609fed2 100644 --- a/token-standard/examples/splice-token-test-dummy-holding/daml.yaml +++ b/token-standard/examples/splice-token-test-dummy-holding/daml.yaml @@ -3,7 +3,7 @@ sdk-version: 3.5.2 name: splice-token-test-dummy-holding -version: 0.0.4 +version: 0.0.5 source: daml dependencies: - daml-prim @@ -14,6 +14,7 @@ data-dependencies: - ../../../daml/dars/splice-api-token-allocation-v1-1.0.0.dar - ../../../daml/dars/splice-api-token-allocation-request-v1-1.0.0.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/examples/splice-token-test-trading-app-v2/daml.yaml b/token-standard/examples/splice-token-test-trading-app-v2/daml.yaml index 995070d761a..e4cf03c8125 100644 --- a/token-standard/examples/splice-token-test-trading-app-v2/daml.yaml +++ b/token-standard/examples/splice-token-test-trading-app-v2/daml.yaml @@ -7,7 +7,7 @@ description: | A trading app PoC built on top of the V2 allocation APIs of the Canton Network Token Standard. Serves both as an example as well as a test suite for the token standard APIs. -version: 1.0.0 +version: 1.0.1 source: daml dependencies: - daml-prim @@ -22,6 +22,7 @@ data-dependencies: - ../../splice-api-token-allocation-instruction-v2/.daml/dist/splice-api-token-allocation-instruction-v2-current.dar - ../../splice-token-standard-utils/.daml/dist/splice-token-standard-utils-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/examples/splice-token-test-trading-app-v2/daml/Splice/Testing/Apps/TradingAppV2.daml b/token-standard/examples/splice-token-test-trading-app-v2/daml/Splice/Testing/Apps/TradingAppV2.daml index fb3be376721..fcf70da20ee 100644 --- a/token-standard/examples/splice-token-test-trading-app-v2/daml/Splice/Testing/Apps/TradingAppV2.daml +++ b/token-standard/examples/splice-token-test-trading-app-v2/daml/Splice/Testing/Apps/TradingAppV2.daml @@ -69,14 +69,14 @@ data AllocationWithContextV1 = AllocationWithContextV1 with extraArgs : ExtraArgs senderAgreementCid : ContractId TradeSettlementAgreement receiverAgreementCid : ContractId TradeSettlementAgreement - deriving (Eq, Show) + deriving (Eq, Show, Serializable) data MissingAllocation = MissingAllocation with authorizerAgreementCid : Optional (ContractId TradeSettlementAgreement) -- ^ None for missing allocations of the venue itself. factoryCid : ContractId V2.AllocationFactory choiceArg : V2.AllocationFactory_Allocate - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A batch of allocations for the same admin together with the extra arguments required to settle them. data SettlementBatch @@ -94,7 +94,7 @@ data SettlementBatch -- which do not create receipt allocations on their own factoryCid : ContractId V2.SettlementFactory extraArgs : ExtraArgs - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Leg of a trade. @@ -103,7 +103,7 @@ data TradeLeg = TradeLeg with -- ^ Admin party of the instrument being transferred. leg : V2.TransferLeg -- ^ The transfer details. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) splitLegsBy : Ord k => (leg -> k) -> [leg] -> Map.Map k [leg] splitLegsBy f legs = Map.fromListWithR (<>) [ (f leg, [leg]) | leg <- legs ] @@ -276,11 +276,13 @@ data OTCTrade_SettleResult = OTCTrade_SettleResult with -- ^ Results for V1-only admins resultsByAdminV2 : Map.Map Party V2.SettlementFactory_SettleBatchResult -- ^ Result for admins that support V2 + deriving (Serializable) data OTCTrade_CancelResult = OTCTrade_CancelResult with resultsV1 : TextMap.TextMap V1.Allocation_CancelResult -- ^ Results for V1 allocation cancellations keyed by transferLegId resultsV2 : [(V2.Account, V2.AllocationResult)] + deriving (Serializable) mkOtcTradeSettlementInfoV2 : ContractId OTCTrade -> OTCTrade -> V2.SettlementInfo mkOtcTradeSettlementInfoV2 otcTradeCid otcTrade = V2.SettlementInfo with diff --git a/token-standard/examples/splice-token-test-trading-app/daml.yaml b/token-standard/examples/splice-token-test-trading-app/daml.yaml index 321b1f48202..d60645fd73c 100644 --- a/token-standard/examples/splice-token-test-trading-app/daml.yaml +++ b/token-standard/examples/splice-token-test-trading-app/daml.yaml @@ -7,7 +7,7 @@ description: | A trading app PoC built on top of the allocation APIs of the Canton Network Token Standard. Serves both as an example as well as a test suite for the token standard APIs. -version: 1.0.2 +version: 1.0.3 source: daml dependencies: - daml-prim @@ -17,6 +17,7 @@ data-dependencies: - ../../../daml/dars/splice-api-token-allocation-v1-1.0.0.dar - ../../../daml/dars/splice-api-token-allocation-request-v1-1.0.0.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/splice-api-token-allocation-instruction-v2/daml.yaml b/token-standard/splice-api-token-allocation-instruction-v2/daml.yaml index 7fa29e52c9e..45b44fae24c 100644 --- a/token-standard/splice-api-token-allocation-instruction-v2/daml.yaml +++ b/token-standard/splice-api-token-allocation-instruction-v2/daml.yaml @@ -13,6 +13,7 @@ data-dependencies: - ../splice-api-token-holding-v2/.daml/dist/splice-api-token-holding-v2-current.dar - ../splice-api-token-allocation-v2/.daml/dist/splice-api-token-allocation-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-allocation-instruction-v2/daml/Splice/Api/Token/AllocationInstructionV2.daml b/token-standard/splice-api-token-allocation-instruction-v2/daml/Splice/Api/Token/AllocationInstructionV2.daml index a3bde9a20b3..b48a411c235 100644 --- a/token-standard/splice-api-token-allocation-instruction-v2/daml/Splice/Api/Token/AllocationInstructionV2.daml +++ b/token-standard/splice-api-token-allocation-instruction-v2/daml/Splice/Api/Token/AllocationInstructionV2.daml @@ -24,7 +24,7 @@ data AllocationInstructionAction AIA_Custom with id : Text -- ^ Identifier of the action. Namespaced analogously to metadata keys. - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) -- | View for `AllocationInstruction`. data AllocationInstructionView = AllocationInstructionView with @@ -71,7 +71,7 @@ data AllocationInstructionView = AllocationInstructionView with meta : Metadata -- ^ Additional metadata specific to the allocation instruction, used for -- extensibility; e.g., more detailed status information. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | An interface for tracking the status of an allocation instruction, -- i.e., a request to a registry app to create an allocation. @@ -133,7 +133,7 @@ data AllocationFactoryView = AllocationFactoryView with -- for which this allocation factory can be used. meta : Metadata -- ^ Additional metadata specific to the allocation factory, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Contracts implementing `AllocationFactory` are retrieved from the registry app and are -- used by the wallet to create allocation instructions (or allocations directly). @@ -203,7 +203,7 @@ data AllocationInstructionResult = AllocationInstructionResult with -- updating multiple allocation instructions in a single Daml transaction. meta : Metadata -- ^ Additional metadata specific to the allocation instruction, used for extensibility; e.g., fees charged. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The output of instructing an allocation or advancing the state of an allocation instruction. data AllocationInstructionResult_Output @@ -220,4 +220,4 @@ data AllocationInstructionResult_Output | AllocationInstructionResult_Failed -- ^ Use this result to communicate that the creation of the allocation did not succeed and -- all holdings reserved for funding the allocation have been released. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) diff --git a/token-standard/splice-api-token-allocation-request-v1/daml.yaml b/token-standard/splice-api-token-allocation-request-v1/daml.yaml index 1c2c98d45ce..5572598150b 100644 --- a/token-standard/splice-api-token-allocation-request-v1/daml.yaml +++ b/token-standard/splice-api-token-allocation-request-v1/daml.yaml @@ -12,6 +12,7 @@ data-dependencies: - ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar - ../splice-api-token-allocation-v1/.daml/dist/splice-api-token-allocation-v1-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-allocation-request-v1/daml/Splice/Api/Token/AllocationRequestV1.daml b/token-standard/splice-api-token-allocation-request-v1/daml/Splice/Api/Token/AllocationRequestV1.daml index cbbe38636fd..8c79f8cf61c 100644 --- a/token-standard/splice-api-token-allocation-request-v1/daml/Splice/Api/Token/AllocationRequestV1.daml +++ b/token-standard/splice-api-token-allocation-request-v1/daml/Splice/Api/Token/AllocationRequestV1.daml @@ -62,4 +62,4 @@ data AllocationRequestView = AllocationRequestView with -- depending on the confidentiality requirements of the app. meta : Metadata -- ^ Additional metadata specific to the allocation request, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) diff --git a/token-standard/splice-api-token-allocation-request-v2/daml.yaml b/token-standard/splice-api-token-allocation-request-v2/daml.yaml index 61963a863eb..5d628f632ef 100644 --- a/token-standard/splice-api-token-allocation-request-v2/daml.yaml +++ b/token-standard/splice-api-token-allocation-request-v2/daml.yaml @@ -13,6 +13,7 @@ data-dependencies: - ../splice-api-token-holding-v2/.daml/dist/splice-api-token-holding-v2-current.dar - ../splice-api-token-allocation-v2/.daml/dist/splice-api-token-allocation-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-allocation-request-v2/daml/Splice/Api/Token/AllocationRequestV2.daml b/token-standard/splice-api-token-allocation-request-v2/daml/Splice/Api/Token/AllocationRequestV2.daml index 083d20e9bf4..b034b4622cd 100644 --- a/token-standard/splice-api-token-allocation-request-v2/daml/Splice/Api/Token/AllocationRequestV2.daml +++ b/token-standard/splice-api-token-allocation-request-v2/daml/Splice/Api/Token/AllocationRequestV2.daml @@ -114,7 +114,7 @@ data AllocationRequestAction ARA_Custom with id : Text -- ^ Identifier of the action. Namespaced analogously to metadata keys. - deriving (Show, Ord, Eq) + deriving (Show, Ord, Eq, Serializable) -- | View of an `AllocationRequest`. data AllocationRequestView = AllocationRequestView with @@ -154,22 +154,22 @@ data AllocationRequestView = AllocationRequestView with -- of action actors that could call a choice SHOULD be included. meta : Metadata -- ^ Additional metadata specific to the allocation request, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The result of the `AllocationRequest_Accept` choice. data AllocationRequest_AcceptResult = AllocationRequest_AcceptResult with meta : Metadata -- ^ Additional metadata specific to the result of accepting the allocation request, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The result of the `AllocationRequest_Reject` choice. data AllocationRequest_RejectResult = AllocationRequest_RejectResult with meta : Metadata -- ^ Additional metadata specific to the result of rejecting the allocation request, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The result of the `AllocationRequest_Withdraw` choice. data AllocationRequest_WithdrawResult = AllocationRequest_WithdrawResult with meta : Metadata -- ^ Additional metadata specific to the result of withdrawing the allocation request, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) diff --git a/token-standard/splice-api-token-allocation-v2/daml.yaml b/token-standard/splice-api-token-allocation-v2/daml.yaml index 2e2bc245816..89f1a37af77 100644 --- a/token-standard/splice-api-token-allocation-v2/daml.yaml +++ b/token-standard/splice-api-token-allocation-v2/daml.yaml @@ -12,6 +12,7 @@ data-dependencies: - ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar - ../splice-api-token-holding-v2/.daml/dist/splice-api-token-holding-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-allocation-v2/daml/Splice/Api/Token/AllocationV2.daml b/token-standard/splice-api-token-allocation-v2/daml/Splice/Api/Token/AllocationV2.daml index 91a73748e5b..c96b0b08516 100644 --- a/token-standard/splice-api-token-allocation-v2/daml/Splice/Api/Token/AllocationV2.daml +++ b/token-standard/splice-api-token-allocation-v2/daml/Splice/Api/Token/AllocationV2.daml @@ -50,7 +50,7 @@ data SettlementInfo = SettlementInfo -- representation being only known after transactions have been prepared. meta : Metadata -- ^ Additional metadata to identify the settlement, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | A specification of a transfer of holdings between two parties for the -- purpose of a settlement, which often requires the atomic execution of multiple legs. @@ -67,7 +67,7 @@ data TransferLeg = TransferLeg with -- ^ The instrument identifier used by the instrument admin. meta : Metadata -- ^ Additional metadata about the transfer leg, used for extensibility. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | A side of a transfer. data TransferSide @@ -75,7 +75,7 @@ data TransferSide -- ^ The outbound side of a transfer, i.e., the sending of assets. | ReceiverSide -- ^ The inbound direction, i.e., the receipt of assets. - deriving (Show, Ord, Eq) + deriving (Show, Ord, Eq, Serializable) -- | A side of a transfer leg, which is what allocations authorize. data TransferLegSide = TransferLegSide with @@ -93,7 +93,7 @@ data TransferLegSide = TransferLegSide with -- ^ The instrument identifier used by the instrument admin. meta : Metadata -- ^ Additional metadata about the transfer leg, used for extensibility. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | An approval by the authorizer to receive or send assets as part of -- settlement. @@ -136,7 +136,7 @@ data AllocationSpecification = AllocationSpecification with meta : Metadata -- ^ Additional metadata for the allocation specification, which can be used -- to store information about an allocation used in iterated settlement. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | Actions available on an allocation. data AllocationAction @@ -147,7 +147,7 @@ data AllocationAction AA_Custom with id : Text -- ^ Identifier of the action. Namespaced analogously to metadata keys. - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) -- | View of a contract representing a ready-to-settle allocation. data AllocationView = AllocationView with @@ -196,7 +196,7 @@ data AllocationView = AllocationView with -- will require appropriate, registry-specific delegation contracts to be in place. meta : Metadata -- ^ Additional metadata specific to the allocation, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- Allocation @@ -215,7 +215,7 @@ data FinalizedAllocation = FinalizedAllocation with -- ^ The funding to reserve for the next settlement iteration. -- -- This MUST NOT be set unless iterated settlement was enabled by the allocation's authorizer. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | A contract representing the approval of the authorizer to send or receive -- the net amount of assets of the transfer legs as part of a settlement where @@ -342,7 +342,7 @@ data AllocationResult = AllocationResult with -- keyed by their `instrumentId.id`. meta : Metadata -- ^ Additional metadata specific to the settlement, used for extensibility. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | The output of changing the state of an allocation. data AllocationResult_Output @@ -360,7 +360,7 @@ data AllocationResult_Output -- ^ The result of the `Allocation_Cancel` choice when fully authorized. | AllocationResult_Withdrawn -- ^ The result of the `Allocation_Withdraw` choice when fully authorized. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- Settlement Factory @@ -373,7 +373,7 @@ data SettlementFactoryView = SettlementFactoryView with -- for which this settlement factory can be used. meta : Metadata -- ^ Additional metadata specific to the settlement factory, used for extensibility. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | A settlement factory enables the net settlement of a batch of allocations -- for the same instrument admin. @@ -451,4 +451,4 @@ data SettlementFactory_SettleBatchResult = SettlementFactory_SettleBatchResult w -- In the same order as the `allocations` in the choice arguments. meta : Metadata -- ^ Additional metadata specific to the batch settlement, used for extensibility. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) diff --git a/token-standard/splice-api-token-holding-v2/daml.yaml b/token-standard/splice-api-token-holding-v2/daml.yaml index f7e93dd6e94..8902a2fd11f 100644 --- a/token-standard/splice-api-token-holding-v2/daml.yaml +++ b/token-standard/splice-api-token-holding-v2/daml.yaml @@ -11,6 +11,7 @@ dependencies: data-dependencies: - ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-holding-v2/daml/Splice/Api/Token/HoldingV2.daml b/token-standard/splice-api-token-holding-v2/daml/Splice/Api/Token/HoldingV2.daml index 41bb064d2c6..44f77837dfe 100644 --- a/token-standard/splice-api-token-holding-v2/daml/Splice/Api/Token/HoldingV2.daml +++ b/token-standard/splice-api-token-holding-v2/daml/Splice/Api/Token/HoldingV2.daml @@ -17,7 +17,7 @@ data InstrumentId = InstrumentId -- ^ The identifier used for the instrument by the instrument admin. -- -- This identifier MUST be unique and unambiguous per instrument admin. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | A data type to represent an on-chain managed account, -- @@ -42,7 +42,7 @@ data Account = Account id : Text -- ^ Account number or similar. -- Use the empty string ("") as the default account identifier. - deriving (Eq, Show, Ord) + deriving (Eq, Show, Ord, Serializable) -- | Details of a lock. data Lock = Lock @@ -65,7 +65,7 @@ data Lock = Lock -- Note that the visibility of the content in this field might be wider -- than the visibility of the contracts in the context. You should thus -- carefully decide what information is safe to put in the lock context. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | Holding interface. interface Holding where viewtype HoldingView @@ -86,4 +86,4 @@ data HoldingView = HoldingView -- transfers to enable a combined unlocking + use choice. meta : Metadata -- ^ Metadata. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) diff --git a/token-standard/splice-api-token-transfer-events-v2/daml.yaml b/token-standard/splice-api-token-transfer-events-v2/daml.yaml index 0d292f2b098..87084f67bff 100644 --- a/token-standard/splice-api-token-transfer-events-v2/daml.yaml +++ b/token-standard/splice-api-token-transfer-events-v2/daml.yaml @@ -12,6 +12,7 @@ data-dependencies: - ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar - ../splice-api-token-holding-v2/.daml/dist/splice-api-token-holding-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-transfer-events-v2/daml/Splice/Api/Token/TransferEventsV2.daml b/token-standard/splice-api-token-transfer-events-v2/daml/Splice/Api/Token/TransferEventsV2.daml index c47f23f3092..dbcaa732ff2 100644 --- a/token-standard/splice-api-token-transfer-events-v2/daml/Splice/Api/Token/TransferEventsV2.daml +++ b/token-standard/splice-api-token-transfer-events-v2/daml/Splice/Api/Token/TransferEventsV2.daml @@ -18,7 +18,7 @@ data TransferSide -- ^ The outbound side of a transfer, i.e., the sending of assets. | ReceiverSide -- ^ The inbound direction, i.e., the receipt of assets. - deriving (Show, Ord, Eq) + deriving (Show, Ord, Eq, Serializable) -- | A side of a transfer of holdings between two parties. -- @@ -39,7 +39,7 @@ data TransferLegSide = TransferLegSide with -- ^ The instrument identifier used by the instrument admin. meta : Metadata -- ^ Additional metadata about the transfer leg, used for extensibility. - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) -- | An interface for templates that can be used to report token standard v2 events. -- @@ -115,8 +115,9 @@ data EventLogView = EventLogView with -- ^ Instrument admin reporting the change in holdings. meta : Metadata -- ^ Additional metadata about the event log, used for extensibility. - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Result of logging a change in holdings event. Provided for extensibility -- in a future where interfaces are upgradeable. data EventLog_HoldingsChangeResult = EventLog_HoldingsChangeResult {} + deriving (Serializable) diff --git a/token-standard/splice-api-token-transfer-instruction-v2/daml.yaml b/token-standard/splice-api-token-transfer-instruction-v2/daml.yaml index ef1e50b9307..bb2f2aa2394 100644 --- a/token-standard/splice-api-token-transfer-instruction-v2/daml.yaml +++ b/token-standard/splice-api-token-transfer-instruction-v2/daml.yaml @@ -12,6 +12,7 @@ data-dependencies: - ../splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar - ../splice-api-token-holding-v2/.daml/dist/splice-api-token-holding-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 codegen: java: diff --git a/token-standard/splice-api-token-transfer-instruction-v2/daml/Splice/Api/Token/TransferInstructionV2.daml b/token-standard/splice-api-token-transfer-instruction-v2/daml/Splice/Api/Token/TransferInstructionV2.daml index ca60f2b223f..53ee3a4c840 100644 --- a/token-standard/splice-api-token-transfer-instruction-v2/daml/Splice/Api/Token/TransferInstructionV2.daml +++ b/token-standard/splice-api-token-transfer-instruction-v2/daml/Splice/Api/Token/TransferInstructionV2.daml @@ -48,7 +48,7 @@ data Transfer = Transfer with -- deliberate contention on holdings to prevent duplicate transfers. meta : Metadata -- ^ Metadata. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The result of instructing a transfer or advancing the state of a transfer instruction. data TransferInstructionResult = TransferInstructionResult with @@ -60,7 +60,7 @@ data TransferInstructionResult = TransferInstructionResult with -- in a single Daml transaction. meta : Metadata -- ^ Additional metadata specific to the transfer instruction, used for extensibility; e.g., fees charged. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | The output of instructing a transfer or advancing the state of a transfer instruction. data TransferInstructionResult_Output @@ -81,7 +81,7 @@ data TransferInstructionResult_Output | TransferInstructionResult_Failed -- ^ Use this result to communicate that the transfer did not succeed and all holdings (minus fees) -- have been returned to the sender. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- TransferInstruction @@ -97,7 +97,7 @@ data TransferInstructionAction TIA_Custom with id : Text -- ^ Identifier of the action. Namespaced analogously to metadata keys. - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Serializable) -- | View for `TransferInstruction`. data TransferInstructionView = TransferInstructionView with @@ -132,7 +132,7 @@ data TransferInstructionView = TransferInstructionView with -- will require appropriate, registry-specific delegation contracts to be in place. meta : Metadata -- ^ Additional metadata specific to the transfer instruction, used for extensibility; e.g., more detailed status information. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) -- | An interface for tracking the status of a transfer instruction, -- i.e., a request to a registry app to execute a transfer. @@ -266,4 +266,4 @@ data TransferFactoryView = TransferFactoryView -- which this transfer factory can be used. meta : Metadata -- ^ Additional metadata specific to the transfer factory, used for extensibility. - deriving (Show, Eq) + deriving (Show, Eq, Serializable) diff --git a/token-standard/splice-token-standard-utils-test/daml.yaml b/token-standard/splice-token-standard-utils-test/daml.yaml index 92161594299..8950ad1e0b9 100644 --- a/token-standard/splice-token-standard-utils-test/daml.yaml +++ b/token-standard/splice-token-standard-utils-test/daml.yaml @@ -5,7 +5,7 @@ sdk-version: 3.5.2 name: splice-token-standard-utils-test description: | Tests of the token-standard-utils library. -version: 1.0.1 +version: 1.0.2 source: daml dependencies: - daml-prim @@ -31,6 +31,7 @@ data-dependencies: - ../splice-token-standard-v1-test/.daml/dist/splice-token-standard-v1-test-current.dar - ../examples/splice-token-test-trading-app-v2/.daml/dist/splice-token-test-trading-app-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/TestAllocationRequestDefaults.daml b/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/TestAllocationRequestDefaults.daml index 56b10c512c3..9eceb8e58c1 100644 --- a/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/TestAllocationRequestDefaults.daml +++ b/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/TestAllocationRequestDefaults.daml @@ -28,6 +28,7 @@ data TestEnv = TestEnv with admin : Party reqCidV1 : ContractId V1.AllocationRequest reqCidV2 : ContractId V2.AllocationRequest + deriving (Serializable) test_allocationRequestV1_Reject : Script () diff --git a/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Allocations.daml b/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Allocations.daml index 73ae5012ce0..76db445acdd 100644 --- a/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Allocations.daml +++ b/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Allocations.daml @@ -510,6 +510,7 @@ data TestEnv = TestEnv with dummyCid : ContractId () dummyCid1 : ContractId () + deriving (Serializable) setupTest : Script TestEnv diff --git a/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Transfers.daml b/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Transfers.daml index 108b9d11cdb..9d1ddcd4ae5 100644 --- a/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Transfers.daml +++ b/token-standard/splice-token-standard-utils-test/daml/Splice/Tests/UnitTest_Transfers.daml @@ -185,6 +185,7 @@ data TestEnv = TestEnv with now : Time transferV2 : V2.Transfer dummyCid : ContractId () + deriving (Serializable) setupTest : Script TestEnv setupTest = do diff --git a/token-standard/splice-token-standard-utils/daml.yaml b/token-standard/splice-token-standard-utils/daml.yaml index 94cc19f4b9b..aaa9688e78c 100644 --- a/token-standard/splice-token-standard-utils/daml.yaml +++ b/token-standard/splice-token-standard-utils/daml.yaml @@ -23,6 +23,7 @@ data-dependencies: - ../splice-api-token-transfer-instruction-v2/.daml/dist/splice-api-token-transfer-instruction-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --force-utility-package=yes diff --git a/token-standard/splice-token-standard-v1-test/daml.yaml b/token-standard/splice-token-standard-v1-test/daml.yaml index 1849c505677..808c4528e7d 100644 --- a/token-standard/splice-token-standard-v1-test/daml.yaml +++ b/token-standard/splice-token-standard-v1-test/daml.yaml @@ -16,7 +16,7 @@ description: | as Daml script code can currently not be shared via .dars across SDKs. The dependencies are normal .dar files and can be shared by copying the .dars. (TODO(#594): remove this limitation) -version: 1.0.16 +version: 1.0.17 source: daml dependencies: - daml-prim @@ -34,6 +34,7 @@ data-dependencies: - ../../daml/splice-amulet/.daml/dist/splice-amulet-current.dar - ../examples/splice-test-token-v1/.daml/dist/splice-test-token-v1-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml b/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml index b78211c3240..27b09c81790 100644 --- a/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml +++ b/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml @@ -95,10 +95,16 @@ defaultAmuletConfig = AmuletConfig with optDevelopmentFundManager = None + developmentFundManagerBlacklist = None + rewardConfig = None transferPreapprovalBaseDuration = None + minDevelopmentFundMintingDelay = None + + amuletSwitchOverTimes = None + -- | Default configuration schedule with single current amulet config defaultAmuletConfigSchedule : Schedule Time (AmuletConfig USD) defaultAmuletConfigSchedule = Schedule with diff --git a/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/TestTokenV1_RegistryV1.daml b/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/TestTokenV1_RegistryV1.daml index 98d2003ae00..9088a720c59 100644 --- a/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/TestTokenV1_RegistryV1.daml +++ b/token-standard/splice-token-standard-v1-test/daml/Splice/Testing/Registries/TestTokenV1_RegistryV1.daml @@ -31,7 +31,7 @@ import Daml.Script -- | A reference to a mock amulet registry. data TestTokenV1Registry = TestTokenV1Registry with admin : Party - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) instance RegistryApi TestTokenV1Registry where getTransferFactory = registryApi_getFactory @@ -51,7 +51,7 @@ instance RegistryApi TestTokenV1Registry where data TestTokenV1RegistryConfig = TestTokenV1RegistryConfig with adminPartyName : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) defaultTestTokenV1RegistryConfig : TestTokenV1RegistryConfig defaultTestTokenV1RegistryConfig = TestTokenV1RegistryConfig with diff --git a/token-standard/splice-token-standard-v2-test/daml.yaml b/token-standard/splice-token-standard-v2-test/daml.yaml index 2892e51e06d..09d814cfe8b 100644 --- a/token-standard/splice-token-standard-v2-test/daml.yaml +++ b/token-standard/splice-token-standard-v2-test/daml.yaml @@ -15,7 +15,7 @@ description: | Must be consumed by copying the source-code into the downstream project, as Daml script code can currently not be shared via .dars across SDKs. (TODO(#594): remove this limitation) -version: 1.0.1 +version: 1.0.2 source: daml dependencies: - daml-prim @@ -51,6 +51,7 @@ data-dependencies: - ../examples/splice-test-token-v2/.daml/dist/splice-test-token-v2-current.dar build-options: + - --explicit-serializable=yes - --target=2.1 - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml b/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml index 04a7817fff1..803e5ff2ce3 100644 --- a/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml +++ b/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml @@ -79,7 +79,7 @@ import Daml.Script data AmuletRegistry = AmuletRegistry with dso : Party instrumentId : InstrumentId - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Serializable) instance RegistryApi AmuletRegistry where getTransferFactory = registryApi_getTransferFactory @@ -113,7 +113,7 @@ data AmuletRegistryConfig = AmuletRegistryConfig initialAmuletPrice : Decimal demoTime : Time dsoPartyName : Text - deriving (Eq, Show) + deriving (Eq, Show, Serializable) -- | Recommended default configuration for setting up the mock amulet registry. defaultAmuletRegistryConfig : AmuletRegistryConfig