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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/build.deployment_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ jobs:
artifactory_user: ${{ vars.ARTIFACTORY_USER }}
artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }}

- name: Deployment test
- name: Helm tests
uses: ./.github/actions/nix/run_bash_command_in_nix
with:
cmd: make cluster/helm/test

- name: Pulumi tests
uses: ./.github/actions/nix/run_bash_command_in_nix
with:
cmd_retry_count: 5 # Retry in case of dependency download errors
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/notify_readme_changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
echo "FOUND_DOCUMENT_CHANGES=true" ;
echo "GITHUB_COMMIT_URL=\"https://github.com/DACH-NY/canton-network-node/commit/${commit}\"" >> "$GITHUB_OUTPUT"
echo "CHANGED_DOC_FILES=\"${changed_files_list}\""
echo "GIT_LOG=\"$(git log -1 --oneline --no-color | sed s/\"//g)\""
echo "GIT_LOG=\"$(git log -1 --oneline --no-color | sed s/\"//g | sed s/\`//g)\""
} >> "$GITHUB_OUTPUT"
else
echo "No document changes are detected."
Expand Down
25 changes: 25 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [Handling Errors in Integration Tests](#handling-errors-in-integration-tests)
- [Connecting external tools to the shared Canton instances](#connecting-external-tools-to-the-shared-canton-instances)
- [Testing App Upgrades](#testing-app-upgrades)
- [Deployment Tests](#deployment-tests)

# Testing in Splice

Expand All @@ -35,6 +36,7 @@ Splice code is tested in the following ways:
- Integration tests. Extensive integration tests are located under `apps/app/src/test/scala/`. Integration tests
include tests that use frontends (whose names must end with `FrontendIntegrationTest`), and ones which do not
(whose names ends with IntegrationTest).
- [Deployment tests](#deployment-tests) to catch errors in Helm and Pulumi before deploying to a cluster.
- Cluster tests. Various different cluster tests are currently run by Digital Asset on Splice codebase.
This includes:
- Deploying and testing a PR on a scratch cluster. See (TBD)
Expand Down Expand Up @@ -365,3 +367,26 @@ PRs/commits that include `[breaking]` in their commit message, or that bump the

The test spins up a full network in the source version, creates some activity, then gradually upgrades several of the components (SVs and validators)
one-by-one to the current commit's version.

## Deployment Tests

Static deployment tests are run on every commit to `main` and on every PR tagged with `[static]` or `[ci]`.
They guard against unintended changes to deployed state resulting from changes to Helm charts and Pulumi deployment scripts.
The tests described here are **not a replacement for testing via cluster deployment**.
They are meant to provide a quick feedback loop and to offer additional protection against regressions for code paths that are not sufficiently well covered by automatic cluster tests.

### Helm checks

We use [helm-unittest](https://github.com/helm-unittest/helm-unittest/) for some of our Helm charts.
To run all Helm chart tests locally run `make cluster/helm/test`.
To run only the tests for a specific chart `CHART`, run `helm unittest cluster/helm/CHART`.

Refer to the documentation of `helm-unittest` for more information on how to extend our Helm tests.
When writing or debugging Helm tests, it is often useful to run `helm template` to see the rendered templates.

### Pulumi checks

Our pulumi checks are based on checked in `expected` files that need to be updated whenever the expected deployment state changes.

Please run `make cluster/pulumi/update-expected` whenever you intend to change Pulumi deployment scripts in a way that alters deployed state.
Compare the diff of the resulting `expected` files to confirm that the changes are as intended.
Original file line number Diff line number Diff line change
Expand Up @@ -593,13 +593,6 @@ object SpliceConfig {
"initialPackageConfig is not valid due to inconsistent dependencies"
),
)
_ <- Either.cond(
conf.synchronizerNodes.isEmpty || conf.supportsSoftDomainMigrationPoc,
(),
ConfigValidationFailed(
"synchronizerNodes must be empty unless supportsSoftDomainMigrationPoc is set to true"
),
)
_ <- Either.cond(
conf.legacyMigrationId.forall(_ == conf.domainMigrationId - 1L),
(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ trait HttpCommandRunner {
* HTTP variant of Canton’s AdminCommandRunner.
*/
protected[console] def httpCommand[Result](
httpCommand: HttpCommand[_, Result]
httpCommand: HttpCommand[_, Result],
basePath: Option[String] = None,
): ConsoleCommandResult[Result]
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,16 @@ trait HttpAppReference extends AppReference with HttpCommandRunner {
def httpClientConfig: NetworkAppClientConfig

override protected[splice] def httpCommand[Result](
httpCommand: HttpCommand[_, Result]
httpCommand: HttpCommand[_, Result],
basePath: Option[String] = None,
): ConsoleCommandResult[Result] =
spliceConsoleEnvironment.httpCommandRunner.runCommand(
name,
httpCommand,
headers,
httpClientConfig,
basePath.fold(httpClientConfig)(p =>
httpClientConfig.copy(url = httpClientConfig.url.withPath(httpClientConfig.url.path + p))
),
)

@Help.Summary("Health and diagnostic related commands (HTTP)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package org.lfdecentralizedtrust.splice.console

import com.digitalasset.canton.SynchronizerAlias
import org.lfdecentralizedtrust.splice.auth.AuthUtil
import org.lfdecentralizedtrust.splice.codegen.java.splice.round.OpenMiningRound
import org.lfdecentralizedtrust.splice.codegen.java.splice.dso.amuletprice as cp
Expand All @@ -24,19 +23,18 @@ import org.lfdecentralizedtrust.splice.sv.{SvApp, SvAppBootstrap, SvAppClientCon
import org.lfdecentralizedtrust.splice.sv.admin.api.client.commands.{
HttpSvAdminAppClient,
HttpSvAppClient,
HttpSvSoftDomainMigrationPocAppClient,
}
import org.lfdecentralizedtrust.splice.sv.automation.{
DsoDelegateBasedAutomationService,
SvDsoAutomationService,
}
import org.lfdecentralizedtrust.splice.sv.config.{SvAppBackendConfig, SvSynchronizerNodeConfig}
import org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig
import org.lfdecentralizedtrust.splice.sv.migration.{DomainDataSnapshot, SynchronizerNodeIdentities}
import org.lfdecentralizedtrust.splice.sv.util.ValidatorOnboarding
import org.lfdecentralizedtrust.splice.util.Contract
import com.digitalasset.canton.admin.api.client.data.NodeStatus
import com.digitalasset.canton.console.{BaseInspection, Help}
import com.digitalasset.canton.topology.{SynchronizerId, ParticipantId, PartyId}
import com.digitalasset.canton.topology.{ParticipantId, PartyId}
import com.digitalasset.canton.tracing.TraceContext
import org.apache.pekko.actor.ActorSystem

Expand Down Expand Up @@ -400,20 +398,6 @@ class SvAppBackendReference(
httpCommand(HttpSvAdminAppClient.GetMediatorNodeStatus())
}

def reconcileSynchronizerDamlState(synchronizerIdPrefix: String): Unit =
consoleEnvironment.run {
httpCommand(
HttpSvSoftDomainMigrationPocAppClient.ReconcileSynchronizerDamlState(synchronizerIdPrefix)
)
}

def signDsoPartyToParticipant(synchronizerIdPrefix: String): Unit =
consoleEnvironment.run {
httpCommand(
HttpSvSoftDomainMigrationPocAppClient.SignDsoPartyToParticipant(synchronizerIdPrefix)
)
}

/** Remote participant this sv app is configured to interact with. */
lazy val participantClient =
new ParticipantClientReference(
Expand All @@ -430,65 +414,21 @@ class SvAppBackendReference(
config.participantClient.participantClientConfigWithAdminToken,
)

def sequencerClient(synchronizerId: SynchronizerId): SequencerClientReference = {
val synchronizerConfig = synchronizerConfigForDomain(synchronizerId)
new SequencerClientReference(
consoleEnvironment,
s"sequencer client for $name for domain $synchronizerId",
synchronizerConfig.sequencer.toCantonConfig,
)
}
private def localSynchronizerNode = config.localSynchronizerNode.getOrElse(
throw new RuntimeException("No synchronizer node configured for SV app")
)

def sequencerClient(synchronizerAlias: SynchronizerAlias): SequencerClientReference = {
val synchronizerConfig: SvSynchronizerNodeConfig = synchronizerConfigForDomain(
synchronizerAlias
)
lazy val sequencerClient: SequencerClientReference =
new SequencerClientReference(
consoleEnvironment,
s"sequencer client for $name for domain $synchronizerAlias",
synchronizerConfig.sequencer.toCantonConfig,
s"sequencer client for $name",
localSynchronizerNode.sequencer.toCantonConfig,
)
}

def mediatorClient(domainId: SynchronizerId): MediatorClientReference = {
val synchronizerConfig: SvSynchronizerNodeConfig = synchronizerConfigForDomain(domainId)
new MediatorClientReference(
consoleEnvironment,
s"mediator client for $name for domain $domainId",
synchronizerConfig.mediator.toCantonConfig,
)
}

def mediatorClient(synchronizerAlias: SynchronizerAlias): MediatorClientReference = {
val synchronizerConfig: SvSynchronizerNodeConfig = synchronizerConfigForDomain(
synchronizerAlias
)
lazy val mediatorClient: MediatorClientReference =
new MediatorClientReference(
consoleEnvironment,
s"mediator client for $name for domain $synchronizerAlias",
synchronizerConfig.mediator.toCantonConfig,
s"mediator client for $name",
localSynchronizerNode.mediator.toCantonConfig,
)
}

private def synchronizerConfigForDomain(alias: SynchronizerAlias) = {
val synchronizerConfig = config.synchronizerNodes.get(alias.toProtoPrimitive) match {
case Some(synchronizer) => synchronizer
case None =>
config.localSynchronizerNode.getOrElse(
throw new RuntimeException("No sequencer admin connection configured for SV App")
)
}
synchronizerConfig
}

private def synchronizerConfigForDomain(domainId: SynchronizerId) = {
val synchronizerConfig = config.synchronizerNodes.get(domainId.uid.identifier.str) match {
case Some(synchronizer) => synchronizer
case None =>
config.localSynchronizerNode.getOrElse(
throw new RuntimeException("No sequencer admin connection configured for SV App")
)
}
synchronizerConfig
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import org.lfdecentralizedtrust.splice.http.v0.definitions.{
SignedTopologyTx,
}
import org.lfdecentralizedtrust.splice.identities.NodeIdentitiesDump
import org.lfdecentralizedtrust.splice.util.ContractWithState
import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient
import org.lfdecentralizedtrust.splice.util.{
ChoiceContextWithDisclosures,
ContractWithState,
FactoryChoiceWithDisclosures,
}
import org.lfdecentralizedtrust.splice.validator.admin.api.client.commands.*
import org.lfdecentralizedtrust.splice.validator.automation.ValidatorAutomationService
import org.lfdecentralizedtrust.splice.validator.config.{
Expand All @@ -25,11 +30,17 @@ import org.lfdecentralizedtrust.splice.validator.config.{
import org.lfdecentralizedtrust.splice.validator.migration.DomainMigrationDump
import org.lfdecentralizedtrust.splice.validator.{ValidatorApp, ValidatorAppBootstrap}
import org.lfdecentralizedtrust.splice.wallet.automation.UserWalletAutomationService
import org.lfdecentralizedtrust.tokenstandard.{metadata, transferinstruction}
import com.digitalasset.canton.console.{BaseInspection, Help}
import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.topology.PartyId
import org.apache.pekko.actor.ActorSystem
import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.TransferPreapproval
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.{
allocationv1,
allocationinstructionv1,
transferinstructionv1,
}
import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletrules.TransferCommandCounter

import java.time.Instant
Expand Down Expand Up @@ -361,6 +372,121 @@ abstract class ValidatorAppReference(
)
}
}

private val scanProxyPrefix = "/api/validator/v0/scan-proxy"

def getRegistryInfo(): metadata.v1.definitions.GetRegistryInfoResponse = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetRegistryInfo,
Some(scanProxyPrefix),
)
}
}

def lookupInstrument(instrumentId: String) =
consoleEnvironment.run {
httpCommand(HttpScanAppClient.LookupInstrument(instrumentId), Some(scanProxyPrefix))
}

def listInstruments() =
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.ListInstruments(pageSize = None, pageToken = None),
Some(scanProxyPrefix),
)
}

def getTransferFactory(
choiceArgs: transferinstructionv1.TransferFactory_Transfer
): (
FactoryChoiceWithDisclosures[
transferinstructionv1.TransferFactory.ContractId,
transferinstructionv1.TransferFactory_Transfer,
],
transferinstruction.v1.definitions.TransferFactoryWithChoiceContext.TransferKind,
) = {
consoleEnvironment.run {
httpCommand(HttpScanAppClient.GetTransferFactory(choiceArgs), Some(scanProxyPrefix))
}
}

def getTransferInstructionAcceptContext(
transferInstructionId: transferinstructionv1.TransferInstruction.ContractId
): ChoiceContextWithDisclosures = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetTransferInstructionAcceptContext(transferInstructionId),
Some(scanProxyPrefix),
)
}
}

def getTransferInstructionRejectContext(
transferInstructionId: transferinstructionv1.TransferInstruction.ContractId
): ChoiceContextWithDisclosures = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetTransferInstructionRejectContext(transferInstructionId),
Some(scanProxyPrefix),
)
}
}

def getTransferInstructionWithdrawContext(
transferInstructionId: transferinstructionv1.TransferInstruction.ContractId
): ChoiceContextWithDisclosures = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetTransferInstructionWithdrawContext(transferInstructionId),
Some(scanProxyPrefix),
)
}
}

def getAllocationFactory(
choiceArgs: allocationinstructionv1.AllocationFactory_Allocate
): FactoryChoiceWithDisclosures[
allocationinstructionv1.AllocationFactory.ContractId,
allocationinstructionv1.AllocationFactory_Allocate,
] = {
consoleEnvironment.run {
httpCommand(HttpScanAppClient.GetAllocationFactory(choiceArgs), Some(scanProxyPrefix))
}
}

def getAllocationTransferContext(
allocationId: allocationv1.Allocation.ContractId
): ChoiceContextWithDisclosures = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetAllocationTransferContext(allocationId),
Some(scanProxyPrefix),
)
}
}

def getAllocationCancelContext(
allocationId: allocationv1.Allocation.ContractId
): ChoiceContextWithDisclosures = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetAllocationCancelContext(allocationId),
Some(scanProxyPrefix),
)
}
}

def getAllocationWithdrawContext(
allocationId: allocationv1.Allocation.ContractId
): ChoiceContextWithDisclosures = {
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetAllocationWithdrawContext(allocationId),
Some(scanProxyPrefix),
)
}
}
}
}

Expand Down
Loading
Loading