Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@martinflorian-da can you take another look? Added a test for the new functionality. In theory there's a slight chance that it will be flaky but I am confident enough that the startup is not fast enough for it to flake.

Copy link
Contributor

@martinflorian-da martinflorian-da Nov 18, 2025

Choose a reason for hiding this comment

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

Ufff IDK, it does sound quite a bit shaky flaky to me... Things change, we'll forget what we did here, then we'll reorder app init order or change something else... and then go debug this.

I guess you run start where you do so it sets up the DB for you? Can't we just do that in the test, before running start?

Yes it seems a bit over the top for this particular PR, but the capability of initialing this store with arbitrary data before we start an app sounds like a handy thing to have also for other tests.

I am confident enough

Not going to veto it but if it was my PR I think I'd lean towards not adding that test, even if the alternative was "I only tested it manually once".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess you run start where you do so it sets up the DB for you? Can't we just do that in the test, before running start?

We could but it's not trivial

Not going to veto it but if it was my PR I think I'd lean towards not adding that test, even if the alternative was "I only tested it manually once".

honestly SGTM, will just remove the test, I was on the fence about it but then realized that our startup is real slow but then again that can always change and we shouldn't count on it

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

...it's also a dangerous precedent... Not everyone's gut feeling is as well calibrated as yours, but they might still end up copying this approach...

Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package org.lfdecentralizedtrust.splice.integration.tests

import com.digitalasset.canton.HasExecutionContext
import com.digitalasset.canton.config.CantonRequireTypes.InstanceName
import com.digitalasset.canton.config.RequireTypes.Port
import com.digitalasset.canton.config.{DbConfig, FullClientConfig}
import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.lifecycle.{CloseContext, FlagCloseable}
import com.digitalasset.canton.logging.{SuppressingLogger, SuppressionRule}
import com.digitalasset.canton.metrics.CommonMockMetrics
import com.digitalasset.canton.resource.DbStorageSingle
import com.digitalasset.canton.topology.{ForceFlag, ParticipantId, PartyId}
import com.typesafe.config.ConfigValueFactory
import org.apache.pekko.http.scaladsl.model.Uri
import org.lfdecentralizedtrust.splice.config.*
import org.lfdecentralizedtrust.splice.config.ConfigTransforms.bumpUrl
import org.lfdecentralizedtrust.splice.config.{
AuthTokenSourceConfig,
NetworkAppClientConfig,
ParticipantBootstrapDumpConfig,
ParticipantClientConfig,
SpliceConfig,
}
import com.digitalasset.canton.logging.SuppressionRule
import org.lfdecentralizedtrust.splice.environment.RetryFor
import org.lfdecentralizedtrust.splice.identities.NodeIdentitiesDump
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
Expand All @@ -22,17 +27,16 @@ import org.lfdecentralizedtrust.splice.validator.config.{
MigrateValidatorPartyConfig,
ValidatorCantonIdentifierConfig,
}
import com.digitalasset.canton.config.CantonRequireTypes.InstanceName
import com.digitalasset.canton.config.{DbConfig, FullClientConfig}
import com.digitalasset.canton.config.RequireTypes.Port
import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.topology.{ForceFlag, ParticipantId, PartyId}
import com.typesafe.config.ConfigValueFactory
import org.apache.pekko.http.scaladsl.model.Uri
import org.lfdecentralizedtrust.splice.validator.migration.ParticipantPartyMigrator
import org.lfdecentralizedtrust.splice.validator.store.{
ValidatorConfigProvider,
ValidatorInternalStore,
}
import org.scalatest.time.{Minute, Span}
import org.slf4j.event.Level

import java.nio.file.{Files, Path, Paths}
import scala.concurrent.duration.DurationInt

trait ValidatorReonboardingIntegrationTestBase
extends IntegrationTest
Expand Down Expand Up @@ -316,7 +320,11 @@ class ValidatorReonboardingIntegrationTest extends ValidatorReonboardingIntegrat
"EXTRA_PARTICIPANT_ADMIN_USER" -> aliceValidatorLocalBackend.config.ledgerApiUser,
"EXTRA_PARTICIPANT_DB" -> newParticipantDb,
) {
loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.WARN))(
loggerFactory.assertLogsSeq(
SuppressionRule.forLogger[ParticipantPartyMigrator] || SuppressionRule
.forLogger[ValidatorConfigProvider] || SuppressionRule
.LevelAndAbove(Level.WARN)
)(
{
aliceValidatorLocalBackend.startSync()
},
Expand All @@ -327,6 +335,16 @@ class ValidatorReonboardingIntegrationTest extends ValidatorReonboardingIntegrat
"not be able to migrate due to an unsupported namespace"
)
}
forExactly(1, entries) {
_.message should include(
"Storing all the hosted parties (4) in the database to recover in case of failures"
)
}
forExactly(1, entries) {
_.message should include(
"Clearing parties that were migrated"
)
}
},
)

Expand Down Expand Up @@ -418,7 +436,7 @@ class ValidatorReonboardingWithPartiesToMigrateIntegrationTest
// (which will fail unless we migrate all parties) despite migrating the operator party.
// This can only work if there is no collision between the participant admin party and
// the validator operator party.
override val aliceValidatorParticipantNameHint = s"${aliceValidatorPartyHint}-participant";
override val aliceValidatorParticipantNameHint = s"$aliceValidatorPartyHint-participant";

// we bootstrap from dump so we can predict the party IDs
val testDumpDir: Path = Paths.get("apps/app/src/test/resources/dumps")
Expand Down Expand Up @@ -459,7 +477,7 @@ class ValidatorReonboardingWithPartiesToMigrateIntegrationTest
testResourcesPath / "standalone-participant-extra-no-auth.conf",
),
Seq.empty,
"alice-participant",
"alice-reonboard-from-key-database",
"EXTRA_PARTICIPANT_ADMIN_USER" -> aliceValidatorLocalBackend.config.ledgerApiUser,
"EXTRA_PARTICIPANT_DB" -> oldParticipantDb,
) {
Expand Down Expand Up @@ -569,3 +587,132 @@ class ValidatorReonboardingWithPartiesToMigrateIntegrationTest
}
}
}

class ValidatorReonboardingWithPartiesToMigrateFromDbIntegrationTest
extends ValidatorReonboardingIntegrationTestBase
with HasExecutionContext {

// avoid db collisions; ptm := partiesToMigrate
override def dbsSuffix = "validator_reonboard_ptm_db"
override def oldParticipantDb = "participant_alice_validator_ptm_db"
override def newParticipantDb = "participant_alice_validator_reonboard_new_ptm_db"
override def appsDb = "splice_apps_reonboard_ptm_db"

// For this test we want to test that we won't try to revoke the domain trust certificate
// (which will fail unless we migrate all parties) despite migrating the operator party.
// This can only work if there is no collision between the participant admin party and
// the validator operator party.
override val aliceValidatorParticipantNameHint = s"${aliceValidatorPartyHint}-participant2";

"re-onboard validator with partiesToMigrate" in { implicit env =>
initDsoWithSv1Only()
// We need a standalone instance to avoid breaking the long-running nodes.
val (dump, aliceValidatorWalletParty, aliceParty, charlieParty) = withCanton(
Seq(
testResourcesPath / "standalone-participant-extra.conf",
testResourcesPath / "standalone-participant-extra-no-auth.conf",
),
Seq.empty,
"alice-participant-rdb",
"EXTRA_PARTICIPANT_ADMIN_USER" -> aliceValidatorLocalBackend.config.ledgerApiUser,
"EXTRA_PARTICIPANT_DB" -> oldParticipantDb,
) {
aliceValidatorBackend.startSync()
val aliceValidatorWalletParty =
PartyId.tryFromProtoPrimitive(aliceValidatorWalletClient.userStatus().party)
aliceValidatorWalletClient.tap(100)

val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend)
aliceWalletClient.tap(100)
val charlieParty = onboardWalletUser(charlieWalletClient, aliceValidatorBackend)
charlieWalletClient.tap(100)

val dump = aliceValidatorBackend.dumpParticipantIdentities()

aliceValidatorBackend.stop()
(dump, aliceValidatorWalletParty, aliceParty, charlieParty)
}

better.files
.File(dumpPath)
.overwrite(
dump.toJson.noSpaces
)

withCanton(
Seq(
testResourcesPath / "standalone-participant-extra.conf",
testResourcesPath / "standalone-participant-extra-no-auth.conf",
),
Seq(),
"alice-reonboard-participant-stored-in-db",
"EXTRA_PARTICIPANT_ADMIN_USER" -> aliceValidatorLocalBackend.config.ledgerApiUser,
"EXTRA_PARTICIPANT_DB" -> newParticipantDb,
) {

loggerFactory.assertLogsSeq(
SuppressionRule.forLogger[ParticipantPartyMigrator] || SuppressionRule
.forLogger[ValidatorConfigProvider] || SuppressionRule
.LevelAndAbove(Level.WARN)
)(
{
aliceValidatorLocalBackend.start()
clue("setting migrating parties in the local db") {
val loggerFactory = SuppressingLogger(getClass)
val closeable = FlagCloseable.withCloseContext(logger, timeouts)
implicit val closeContext: CloseContext = closeable.closeContext
val provider = new ValidatorConfigProvider(
ValidatorInternalStore(
DbStorageSingle.tryCreate(
aliceValidatorLocalBackend.config.storage,
wallClock,
None,
connectionPoolForParticipant = false,
None,
CommonMockMetrics.dbStorage,
timeouts,
loggerFactory,
),
loggerFactory,
),
loggerFactory,
)
eventuallySucceeds(30.seconds, 20.millis, suppressErrors = false) {
provider
.setPartiesToMigrate(
Set(aliceParty, aliceValidatorWalletParty)
)
.futureValue
}
closeable.close()
}
aliceValidatorLocalBackend.waitForInitialization()
},
entries => {
forExactly(1, entries) {
_.message should include(
"hosted parties in the local database, this indicates a retry of the migration process"
)
}
},
)

aliceValidatorLocalBackend.appState.configProvider
.getPartiesToMigrate()
.value
.futureValue shouldBe None

Seq(
aliceValidatorWalletParty,
aliceParty,
).foreach { partyId =>
clue(s"check mapping and amulet balance of $partyId") {
assertMapping(
partyId,
aliceValidatorLocalBackend.participantClient.id,
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1384,8 +1384,8 @@ abstract class TopologyAdminConnection(
runCmd(VaultAdminCommands.ImportKeyPair(ByteString.copyFrom(keyPair), name, password = None))
}

private def listSynchronizerTrustCertificate(synchronizerId: SynchronizerId, member: Member)(
implicit tc: TraceContext
def listSynchronizerTrustCertificate(synchronizerId: SynchronizerId, member: Member)(implicit
tc: TraceContext
): Future[Seq[TopologyResult[SynchronizerTrustCertificate]]] =
runCmd(
TopologyAdminCommands.Read.ListSynchronizerTrustCertificate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object FutureUnlessShutdownUtil {
)(implicit ec: ExecutionContext): OptionT[Future, A] = {
OptionT(futureUnlessShutdownToFuture(f.value))
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Very important to have newlines! 😛

implicit class FutureUnlessShutdownOps[A](val f: FutureUnlessShutdown[A]) extends AnyVal {
def toFuture(implicit ec: ExecutionContext): Future[A] =
f.failOnShutdownToAbortException("Splice unsafe shutdown future")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import org.apache.pekko.stream.Materializer
import org.apache.pekko.stream.scaladsl.{Sink, Source}
import org.apache.pekko.stream.testkit.StreamSpec
import org.lfdecentralizedtrust.splice.admin.api.client.commands.HttpCommandException
import org.lfdecentralizedtrust.splice.util.{SpliceRateLimitMetrics, SpliceRateLimiter}
Copy link
Contributor

Choose a reason for hiding this comment

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

Just going to pretend I understand how this passed static checks previously...

import org.lfdecentralizedtrust.splice.util.SpliceRateLimiterTest.runRateLimited

import scala.concurrent.Future
Expand Down
Loading