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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ class SvAppBackendReference(
)
}

@Help.Summary("Grant a ValidatorLicense to a validator party (via admin API)")
def grantValidatorLicense(partyId: PartyId): Unit =
consoleEnvironment.run {
httpCommand(
HttpSvAdminAppClient.GrantValidatorLicense(partyId)
)
}

@Help.Summary("Update CC price vote (via admin API)")
def updateAmuletPriceVote(amuletPrice: BigDecimal): Unit =
consoleEnvironment.run {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,18 @@ class BootstrapPackageConfigIntegrationTest
// This simulates an app vetting newer versions of their own DARs depending on newer splice-amulet versions
// before the SVs do so. Topology aware package selection will then force the old splice-amulet and old splitwell versions
// for composed transactions. Note that for this to work splitwell contracts must be downgradeable.

// Split into batches to avoid gRPC message size limit (10 MB)
val batchSize = 12
val versionBatches =
DarResources.splitwell.all.map(_.metadata.version).distinct.grouped(batchSize).toSeq

Seq(aliceValidatorBackend, bobValidatorBackend, splitwellValidatorBackend).foreach { p =>
p.participantClient.dars.upload_many(
DarResources.splitwell.all
.map(_.metadata.version)
.distinct
.map((v: PackageVersion) => s"daml/dars/splitwell-$v.dar")
)
versionBatches.foreach { batch =>
p.participantClient.dars.upload_many(
batch.map((v: PackageVersion) => s"daml/dars/splitwell-$v.dar")
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,56 @@ class SvFrontendIntegrationTest
}
}

"can grant a validator license to an existing party" in { implicit env =>
withFrontEnd("sv1") { implicit webDriver =>
actAndCheck(
"sv1 operator can login and browse to the validator-onboarding tab", {
go to s"http://localhost:$sv1UIPort/validator-onboarding"
loginOnCurrentPage(sv1UIPort, sv1Backend.config.ledgerApiUser)
},
)(
"We see the grant validator license form",
_ => {
find(id("grant-license-party-address")) should not be empty
find(id("grant-validator-license")) should not be empty
},
)

val licenseRows = getLicensesTableRows
val newValidatorParty = allocateRandomSvParty("test-validator", Some(100))

actAndCheck(
"fill party address", {
inside(find(id("grant-license-party-address"))) { case Some(element) =>
element.underlying.sendKeys(newValidatorParty.toProtoPrimitive)
}
},
)(
"grant button becomes enabled",
_ => {
find(id("grant-validator-license")).value.isEnabled shouldBe true
},
)

actAndCheck(
"click the grant validator license button", {
click on "grant-validator-license"

click on "grant-license-confirmation-dialog-accept-button"
},
)(
"a new validator license row is added",
_ => {
checkValidatorLicenseRow(
licenseRows.size.toLong,
sv1Backend.getDsoInfo().svParty,
newValidatorParty,
)
},
)
}
}

"can view median amulet price and update desired amulet price by each SV" in { implicit env =>
withFrontEnd("sv1") { implicit webDriver =>
actAndCheck(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package org.lfdecentralizedtrust.splice.integration.tests

import com.digitalasset.canton.logging.SuppressionRule
import com.digitalasset.canton.topology.PartyId
import org.lfdecentralizedtrust.splice.codegen.java.splice.validatorlicense.ValidatorLicense
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.MergeValidatorLicenseContractsTrigger
import org.lfdecentralizedtrust.splice.util.TriggerTestUtil
import org.lfdecentralizedtrust.splice.util.TriggerTestUtil.{
pauseAllDsoDelegateTriggers,
resumeAllDsoDelegateTriggers,
}
import org.slf4j.event.Level
import scala.jdk.OptionConverters.*

class SvValidatorLicenseIntegrationTest
extends SvIntegrationTestBase
with TriggerTestUtil
with ExternallySignedPartyTestUtil {

override def environmentDefinition: EnvironmentDefinition =
EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName)

"grant validator license and verify merged licenses" in { implicit env =>
val info = sv1Backend.getDsoInfo()
val dsoParty = info.dsoParty
val sv1Party = info.svParty

def getLicensesFromAliceValidator(p: PartyId) = {
aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs
.filterJava(ValidatorLicense.COMPANION)(
dsoParty,
c => c.data.validator == p.toProtoPrimitive,
)
}

// Allocate a new external party on aliceValidator
val OnboardingResult(newParty, _, _) =
onboardExternalParty(aliceValidatorBackend, Some("alice-test-party-2"))

// Test 1: Can allocate license to a new party on aliceValidator

val licenses = getLicensesFromAliceValidator(newParty)
licenses should have length 0

actAndCheck(
"Grant validator license to random new party on aliceValidator",
sv1Backend.grantValidatorLicense(newParty),
)(
"ValidatorLicense granted to a random new party",
_ => {
val licenses = getLicensesFromAliceValidator(newParty)

licenses should have length 1
licenses.head.data.validator shouldBe newParty.toProtoPrimitive
licenses.head.data.sponsor shouldBe sv1Party.toProtoPrimitive
},
)

// Test 2: Verify that granting new license to existing validator merges all the
// licenses to one, and does not affect its kind

val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId()
val initialAliceLicenses = getLicensesFromAliceValidator(aliceValidatorParty)
initialAliceLicenses should have length 1
val initialKind = initialAliceLicenses.head.data.kind.toScala

// Pause the merge trigger to confirm multiple licenses exist
pauseAllDsoDelegateTriggers[MergeValidatorLicenseContractsTrigger]

// Grant a license to Alice (creates NonOperatorLicense) with trigger paused
actAndCheck(
"Grant license to an onboarded validator",
sv1Backend.grantValidatorLicense(aliceValidatorParty),
)(
"Two ValidatorLicenses exist for alice validator party",
_ => {
val licenses = getLicensesFromAliceValidator(aliceValidatorParty)

licenses should have length 2
},
)

// Resume the merge trigger and verify licenses got merged
// The trigger can process both validator licenses in parallel so we might get multiple log messages.
loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.WARN))(
{
actAndCheck(
"Resume merge trigger",
resumeAllDsoDelegateTriggers[MergeValidatorLicenseContractsTrigger],
)(
"Licenses are merged while maintaining kind",
_ => {
val licenses = getLicensesFromAliceValidator(aliceValidatorParty)

licenses should have length 1
licenses.head.data.validator shouldBe aliceValidatorParty.toProtoPrimitive
licenses.head.data.kind.toScala shouldBe initialKind
},
)
// Pause to make sure we don't get more log messages
pauseAllDsoDelegateTriggers[MergeValidatorLicenseContractsTrigger]
},
forAll(_)(
_.warningMessage should include(
"has 2 Validator License contracts."
)
),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { rest, RestHandler } from 'msw';

import { ValidatorLicense } from '@daml.js/splice-amulet/lib/Splice/ValidatorLicense';
import { LicenseKind, ValidatorLicense } from '@daml.js/splice-amulet/lib/Splice/ValidatorLicense';

export function validatorLicensesHandler(baseUrl: string): RestHandler {
return rest.get(`${baseUrl}/v0/admin/validator/licenses`, (req, res, ctx) => {
Expand All @@ -10,6 +10,26 @@ export function validatorLicensesHandler(baseUrl: string): RestHandler {
const aTimestamp = '2024-09-26T16:15:36Z';
const validatorLicenses = Array.from({ length: n }, (_, i) => {
const id = (i + from).toString();
const index = i + from;

// Vary weights: default (null), 1.5, 2.0, and 0.0
let weight: string | null = null;
if (index % 4 === 1) {
weight = '1.5';
} else if (index % 4 === 2) {
weight = '2.0';
} else if (index % 4 === 3) {
weight = '0.0';
}

// Vary kinds: null (default OperatorLicense), OperatorLicense, NonOperatorLicense
let kind: LicenseKind | null = null;
if (index % 3 === 1) {
kind = 'OperatorLicense';
} else if (index % 3 === 2) {
kind = 'NonOperatorLicense';
}

const validatorLicense: ValidatorLicense = {
dso: 'dso',
validator: `validator::${id}`,
Expand All @@ -21,6 +41,8 @@ export function validatorLicensesHandler(baseUrl: string): RestHandler {
},
metadata: { version: '1', lastUpdatedAt: aTimestamp, contactPoint: 'nowhere' },
lastActiveAt: aTimestamp,
weight,
kind,
};
return {
contract_id: id,
Expand Down
Loading
Loading