From 2bcbcf080e7aa7757127d76f6ab43fde226d6cf1 Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Mon, 5 Oct 2026 23:34:14 +0600 Subject: [PATCH 1/2] test: admit a participant to a running dedicated synchronizer [ci] The sync operator integration test restricts splitwell to permissioned participants the way the LocalNet bootstrap does, permissioning the connected participant first. A participant outside that set is refused, and joins once the owner permissions it. Splitwell is opened again at the end, since the other tests in the job share its Canton. Signed-off-by: sadiq1971 --- .../tests/SyncOperatorIntegrationTest.scala | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala index 0e8d79dbc9a..189c6311c4f 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala @@ -3,12 +3,24 @@ package org.lfdecentralizedtrust.splice.integration.tests -import com.digitalasset.canton.SynchronizerAlias +import com.digitalasset.canton.{SequencerAlias, SynchronizerAlias} +import com.digitalasset.canton.admin.api.client.data.SynchronizerConnectionConfig +import com.digitalasset.canton.config.RequireTypes.PositiveInt +import com.digitalasset.canton.protocol.OnboardingRestriction +import com.digitalasset.canton.topology.ParticipantId +import com.digitalasset.canton.topology.admin.grpc.TopologyStoreId +import com.digitalasset.canton.topology.transaction.{ + ParticipantPermission, + ParticipantSynchronizerPermission, + TopologyMapping, +} import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest class SyncOperatorIntegrationTest extends IntegrationTest { + private val splitwellAlias = SynchronizerAlias.tryCreate("splitwell") + override def environmentDefinition: SpliceEnvironmentDefinition = EnvironmentDefinition .fromResources( @@ -36,5 +48,85 @@ class SyncOperatorIntegrationTest extends IntegrationTest { .logical syncOperatorBackend.appState.store.key.synchronizerId shouldBe served } + + "admit a participant to its running synchronizer" in { implicit env => + // The splitwell sequencer holds the synchronizer's owner key. + val owner = syncOperatorBackend.appState.sequencerAdminConnection + val synchronizerId = syncOperatorBackend.appState.store.key.synchronizerId + val alice = aliceValidatorBackend.participantClientWithAdminToken + val bob = bobValidatorBackend.participantClientWithAdminToken + + def permission(participantId: ParticipantId) = + ParticipantSynchronizerPermission( + synchronizerId, + participantId, + ParticipantPermission.Submission, + limits = None, + loginAfter = None, + ) + + def permit(participantId: ParticipantId): Unit = { + owner + .proposeMapping( + TopologyStoreId.Synchronizer(synchronizerId), + permission(participantId), + serial = PositiveInt.one, + isProposal = false, + ) + .futureValue + // The sequencer checks a joining participant against its own topology store. + eventually() { + owner + .listAllTransactions( + TopologyStoreId.Synchronizer(synchronizerId), + includeMappings = Set(TopologyMapping.Code.ParticipantSynchronizerPermission), + ) + .futureValue + .map(_.mapping) should contain(permission(participantId)) + } + } + + def setOnboardingRestriction(restriction: OnboardingRestriction): Unit = + owner + .ensureDomainParameters(synchronizerId, _.tryUpdate(onboardingRestriction = restriction)) + .futureValue + + clue("the owner permissions the participant already connected, then restricts onboarding") { + permit(alice.id) + setOnboardingRestriction(OnboardingRestriction.RestrictedOpen) + } + + // Bob's validator does not connect to splitwell, so his participant is given the sequencer + // alice's validator connects to. + bob.synchronizers.register_by_config( + SynchronizerConnectionConfig.tryGrpcSingleConnection( + splitwellAlias, + SequencerAlias.Default, + aliceValidatorBackend.config.domains.extra.find(_.alias == splitwellAlias).value.url, + manualConnect = true, + ), + performHandshake = false, + synchronize = None, + ) + + clue("a participant the owner has not permissioned is refused") { + assertThrowsAndLogsCommandFailures( + bob.synchronizers.reconnect(splitwellAlias, retry = false, synchronize = None), + _.errorMessage should include("INITIAL_ONBOARDING_ERROR"), + ) + } + + clue("once the owner permissions it, the same participant joins") { + permit(bob.id) + bob.synchronizers.reconnect(splitwellAlias, synchronize = None) shouldBe true + eventually() { + bob.synchronizers.active(splitwellAlias) shouldBe true + } + } + + // The other tests in this CI job share this Canton and expect splitwell open to anyone. + bob.synchronizers.disconnect(splitwellAlias) + setOnboardingRestriction(OnboardingRestriction.UnrestrictedOpen) + } } } From fc01840410c7be3e53bfeb3ffadc9e3d68562f57 Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Tue, 6 Oct 2026 19:14:08 +0600 Subject: [PATCH 2/2] test: check onboarding while open and while restricted [ci] A full connect pushes the participant's own topology, which needs traffic on the zero-base-rate splitwell, so the joins run only the onboarding handshake and are read from the owner's topology. The test also checks that a participant joins without a permission while onboarding is open. Signed-off-by: sadiq1971 --- .../tests/SyncOperatorIntegrationTest.scala | 101 ++++++++++-------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala index 189c6311c4f..48253cac935 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SyncOperatorIntegrationTest.scala @@ -14,6 +14,7 @@ import com.digitalasset.canton.topology.transaction.{ ParticipantSynchronizerPermission, TopologyMapping, } +import org.lfdecentralizedtrust.splice.console.ParticipantClientReference import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest @@ -49,41 +50,60 @@ class SyncOperatorIntegrationTest extends IntegrationTest { syncOperatorBackend.appState.store.key.synchronizerId shouldBe served } - "admit a participant to its running synchronizer" in { implicit env => + "admit participants as its onboarding restriction allows" in { implicit env => // The splitwell sequencer holds the synchronizer's owner key. val owner = syncOperatorBackend.appState.sequencerAdminConnection val synchronizerId = syncOperatorBackend.appState.store.key.synchronizerId val alice = aliceValidatorBackend.participantClientWithAdminToken + // Neither of these participants is on splitwell in this topology. val bob = bobValidatorBackend.participantClientWithAdminToken + val splitwellParticipant = splitwellValidatorBackend.participantClientWithAdminToken - def permission(participantId: ParticipantId) = - ParticipantSynchronizerPermission( - synchronizerId, - participantId, - ParticipantPermission.Submission, - limits = None, - loginAfter = None, + val splitwellConnection = SynchronizerConnectionConfig.tryGrpcSingleConnection( + splitwellAlias, + SequencerAlias.Default, + aliceValidatorBackend.config.domains.extra.find(_.alias == splitwellAlias).value.url, + manualConnect = true, + ) + + // Only the onboarding handshake, which needs no traffic on this zero-base-rate synchronizer. + def join(participant: ParticipantClientReference): Unit = + participant.synchronizers.register_by_config( + splitwellConnection, + performHandshake = true, + synchronize = None, ) + def joined(participantId: ParticipantId): Boolean = + owner.listSynchronizerTrustCertificate(synchronizerId, participantId).futureValue.nonEmpty + + def permitted(participantId: ParticipantId): Boolean = + owner + .listAllTransactions( + TopologyStoreId.Synchronizer(synchronizerId), + includeMappings = Set(TopologyMapping.Code.ParticipantSynchronizerPermission), + ) + .futureValue + .flatMap(_.selectMapping[ParticipantSynchronizerPermission]) + .exists(_.mapping.participantId == participantId) + def permit(participantId: ParticipantId): Unit = { owner .proposeMapping( TopologyStoreId.Synchronizer(synchronizerId), - permission(participantId), + ParticipantSynchronizerPermission( + synchronizerId, + participantId, + ParticipantPermission.Submission, + limits = None, + loginAfter = None, + ), serial = PositiveInt.one, isProposal = false, ) .futureValue // The sequencer checks a joining participant against its own topology store. - eventually() { - owner - .listAllTransactions( - TopologyStoreId.Synchronizer(synchronizerId), - includeMappings = Set(TopologyMapping.Code.ParticipantSynchronizerPermission), - ) - .futureValue - .map(_.mapping) should contain(permission(participantId)) - } + eventually()(permitted(participantId) shouldBe true) } def setOnboardingRestriction(restriction: OnboardingRestriction): Unit = @@ -91,41 +111,36 @@ class SyncOperatorIntegrationTest extends IntegrationTest { .ensureDomainParameters(synchronizerId, _.tryUpdate(onboardingRestriction = restriction)) .futureValue - clue("the owner permissions the participant already connected, then restricts onboarding") { - permit(alice.id) - setOnboardingRestriction(OnboardingRestriction.RestrictedOpen) + clue("while onboarding is open, a participant joins without a permission") { + owner + .getSynchronizerParametersState(synchronizerId) + .futureValue + .mapping + .parameters + .onboardingRestriction shouldBe OnboardingRestriction.UnrestrictedOpen + // Alice's validator joined splitwell when the topology started. + joined(alice.id) shouldBe true + permitted(alice.id) shouldBe false } - // Bob's validator does not connect to splitwell, so his participant is given the sequencer - // alice's validator connects to. - bob.synchronizers.register_by_config( - SynchronizerConnectionConfig.tryGrpcSingleConnection( - splitwellAlias, - SequencerAlias.Default, - aliceValidatorBackend.config.domains.extra.find(_.alias == splitwellAlias).value.url, - manualConnect = true, - ), - performHandshake = false, - synchronize = None, - ) - - clue("a participant the owner has not permissioned is refused") { + clue("while onboarding is restricted, a participant without a permission is refused") { + // The participant already on splitwell is permissioned first, so it keeps its access. + permit(alice.id) + setOnboardingRestriction(OnboardingRestriction.RestrictedOpen) assertThrowsAndLogsCommandFailures( - bob.synchronizers.reconnect(splitwellAlias, retry = false, synchronize = None), + join(bob), _.errorMessage should include("INITIAL_ONBOARDING_ERROR"), ) + joined(bob.id) shouldBe false } - clue("once the owner permissions it, the same participant joins") { - permit(bob.id) - bob.synchronizers.reconnect(splitwellAlias, synchronize = None) shouldBe true - eventually() { - bob.synchronizers.active(splitwellAlias) shouldBe true - } + clue("while onboarding is restricted, a participant the owner permissions joins") { + permit(splitwellParticipant.id) + join(splitwellParticipant) + eventually()(joined(splitwellParticipant.id) shouldBe true) } // The other tests in this CI job share this Canton and expect splitwell open to anyone. - bob.synchronizers.disconnect(splitwellAlias) setOnboardingRestriction(OnboardingRestriction.UnrestrictedOpen) } }