Skip to content

Commit e932fdb

Browse files
Add option to allow specific scans in the validator (#2825)
* Enable TrustSpecific for scan-client Part of #2409 1) New Configuration Type: TrustSpecific 2) New Connection Strategy: TrustSpecificScanList 3) Factory Update: The apply Method 4) Consensus threshold: Modifying BftCallConfig.default [ci] Signed-off-by: Pasindu Tennage <pasindu.tennage@digitalasset.com>
1 parent 937aa1d commit e932fdb

File tree

9 files changed

+630
-97
lines changed

9 files changed

+630
-97
lines changed

apps/app/src/main/scala/org/lfdecentralizedtrust/splice/config/SpliceConfig.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ object SpliceConfig {
427427
implicit val scanClientConfigTrustSingleConfigReader
428428
: ConfigReader[BftScanClientConfig.TrustSingle] =
429429
deriveReader[BftScanClientConfig.TrustSingle]
430+
implicit val scanClientConfigBftCustomConfigReader
431+
: ConfigReader[BftScanClientConfig.BftCustom] =
432+
deriveReader[BftScanClientConfig.BftCustom]
430433
implicit val scanClientConfigSeedsConfigReader: ConfigReader[BftScanClientConfig.Bft] =
431434
deriveReader[BftScanClientConfig.Bft]
432435
implicit val scanClientConfigConfigReader: ConfigReader[BftScanClientConfig] =
@@ -847,6 +850,9 @@ object SpliceConfig {
847850
implicit val scanClientConfigTrustSingleConfigWriter
848851
: ConfigWriter[BftScanClientConfig.TrustSingle] =
849852
deriveWriter[BftScanClientConfig.TrustSingle]
853+
implicit val scanClientConfigBftCustomConfigWriter
854+
: ConfigWriter[BftScanClientConfig.BftCustom] =
855+
deriveWriter[BftScanClientConfig.BftCustom]
850856
implicit val scanClientConfigSeedsConfigWriter: ConfigWriter[BftScanClientConfig.Bft] =
851857
deriveWriter[BftScanClientConfig.Bft]
852858
implicit val scanClientConfigConfigWriter: ConfigWriter[BftScanClientConfig] =

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/plugins/UseToxiproxy.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,21 @@ case class UseToxiproxy(
175175
BftScanClientConfig.TrustSingle(newUrl, amuletRulesCacheTimeToLive)
176176
),
177177
)
178+
case BftScanClientConfig
179+
.BftCustom(
180+
seedUrls,
181+
_,
182+
_,
183+
amuletRulesCacheTimeToLive,
184+
scansRefreshInterval,
185+
) =>
186+
val newUrl = addScanAppHttpProxy(n.unwrap, seedUrls.head, basePortBump)
187+
(
188+
n,
189+
config.copy(scanClient =
190+
BftScanClientConfig.TrustSingle(newUrl, amuletRulesCacheTimeToLive)
191+
),
192+
)
178193
}
179194
}
180195
.toMap
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package org.lfdecentralizedtrust.splice.integration.tests
4+
5+
import cats.data.NonEmptyList
6+
import com.digitalasset.canton.{HasActorSystem, HasExecutionContext}
7+
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
8+
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest
9+
import org.lfdecentralizedtrust.splice.util.{SvTestUtil, WalletTestUtil}
10+
import org.lfdecentralizedtrust.splice.config.ConfigTransforms
11+
import org.apache.pekko.http.scaladsl.model.Uri
12+
import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection.BftScanClientConfig
13+
import com.digitalasset.canton.config.NonNegativeFiniteDuration
14+
import com.digitalasset.canton.logging.SuppressionRule
15+
import org.slf4j.event.Level
16+
17+
class ConfigurationProvidedBftScanConnectionIntegrationTest
18+
extends IntegrationTest
19+
with WalletTestUtil
20+
with SvTestUtil
21+
with HasExecutionContext
22+
with HasActorSystem {
23+
24+
override protected def runEventHistorySanityCheck: Boolean = false
25+
override protected def runUpdateHistorySanityCheck: Boolean = false
26+
27+
override def environmentDefinition: SpliceEnvironmentDefinition =
28+
EnvironmentDefinition
29+
.simpleTopology4Svs(this.getClass.getSimpleName)
30+
.addConfigTransforms((_, config) =>
31+
ConfigTransforms.updateAllValidatorConfigs {
32+
case (name, c) if name == "aliceValidator" =>
33+
val bftCustomConfig = BftScanClientConfig.BftCustom(
34+
seedUrls = NonEmptyList.of(
35+
Uri("http://127.0.0.1:5012"),
36+
Uri("http://127.0.0.1:5112"),
37+
),
38+
trustedSvs =
39+
NonEmptyList.of(s"${getSvName(1)}", s"${getSvName(2)}", s"${getSvName(3)}"),
40+
threshold = Some(2),
41+
scansRefreshInterval = NonNegativeFiniteDuration.ofSeconds(5),
42+
)
43+
c.copy(scanClient = bftCustomConfig)
44+
case (_, c) => c
45+
}(config)
46+
)
47+
.withManualStart
48+
49+
def connectionEstablished(svName: Int, message: Seq[String]) = {
50+
message.exists(
51+
_.contains(
52+
s"Successfully established initial connection to trusted scan: ${getSvName(svName)}"
53+
)
54+
) should be(true)
55+
}
56+
57+
def connectionNotEstablished(svName: Int, message: Seq[String]) = {
58+
message.exists(
59+
_.contains(
60+
s"Successfully established initial connection to trusted scan: ${getSvName(svName)}"
61+
)
62+
) should be(false)
63+
}
64+
65+
def refreshConnectionEstablished(svName: Int, message: Seq[String]) = {
66+
message.exists(
67+
_.contains(
68+
s"Successfully connected to scan of ${getSvName(svName)}"
69+
)
70+
) should be(true)
71+
}
72+
73+
"simple threshold normal case validator onboarding succeeds" in { implicit env =>
74+
startAllSync(
75+
sv1Backend,
76+
sv1ScanBackend,
77+
sv2Backend,
78+
sv2ScanBackend,
79+
sv3Backend,
80+
sv3ScanBackend,
81+
sv4Backend,
82+
sv4ScanBackend,
83+
)
84+
85+
eventually() {
86+
val allHealthy = Seq(sv1ScanBackend, sv2ScanBackend, sv3ScanBackend, sv4ScanBackend).forall {
87+
scan =>
88+
scan.httpHealth.successOption.exists(_.active)
89+
}
90+
allHealthy shouldBe true
91+
}
92+
93+
loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))(
94+
{
95+
// start alice validator
96+
aliceValidatorBackend.startSync()
97+
},
98+
logs => {
99+
val messages = logs.map(_.message)
100+
withClue("Validator should connect to sv1:") {
101+
connectionEstablished(1, messages)
102+
}
103+
withClue("Validator should connect to sv2:") {
104+
connectionEstablished(2, messages)
105+
}
106+
withClue("Validator should connect to sv3:") {
107+
connectionEstablished(3, messages)
108+
}
109+
withClue("Validator should NOT connect to sv4:") {
110+
connectionNotEstablished(4, messages)
111+
}
112+
},
113+
)
114+
115+
withClue("Alice's validator should be able to onboard a user after establishing connections.") {
116+
eventuallySucceeds() {
117+
aliceValidatorBackend.onboardUser(aliceWalletClient.config.ledgerApiUser)
118+
}
119+
}
120+
}
121+
122+
"reconnects to a recovered SV after the refresh interval" in { implicit env =>
123+
startAllSync(sv1Backend, sv1ScanBackend, sv2Backend, sv2ScanBackend, sv4Backend, sv4ScanBackend)
124+
125+
eventually() {
126+
val allHealthy = Seq(sv1ScanBackend, sv2ScanBackend, sv4ScanBackend).forall { scan =>
127+
scan.httpHealth.successOption.exists(_.active)
128+
}
129+
allHealthy shouldBe true
130+
}
131+
132+
loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))(
133+
{
134+
aliceValidatorBackend.startSync()
135+
},
136+
logs => {
137+
val messages = logs.map(_.message)
138+
withClue("Validator should connect to sv1:") {
139+
connectionEstablished(1, messages)
140+
}
141+
withClue("Validator should connect to sv2:") {
142+
connectionEstablished(2, messages)
143+
}
144+
withClue("Validator should not connect to the offline sv3:") {
145+
connectionNotEstablished(3, messages)
146+
}
147+
withClue("Validator should not connect to sv4") {
148+
connectionNotEstablished(4, messages)
149+
}
150+
},
151+
)
152+
153+
loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))(
154+
{
155+
sv3Backend.startSync()
156+
sv3ScanBackend.startSync()
157+
},
158+
logs => {
159+
val messages = logs.map(_.message)
160+
withClue("Validator should detect and connect to the newly started sv3 after a refresh:") {
161+
refreshConnectionEstablished(3, messages)
162+
}
163+
},
164+
)
165+
166+
withClue(
167+
"The validator should successfully onboard a user after connecting to the recovered scan."
168+
) {
169+
eventuallySucceeds() {
170+
aliceValidatorBackend.onboardUser(aliceWalletClient.config.ledgerApiUser)
171+
}
172+
}
173+
}
174+
175+
"validator crashes when the number of connected scans drops below the threshold" in {
176+
implicit env =>
177+
startAllSync(
178+
sv1Backend,
179+
sv1ScanBackend,
180+
sv2Backend,
181+
sv2ScanBackend,
182+
sv3Backend,
183+
sv3ScanBackend,
184+
sv4Backend,
185+
sv4ScanBackend,
186+
)
187+
188+
eventually() {
189+
val allHealthy =
190+
Seq(sv1ScanBackend, sv2ScanBackend, sv3ScanBackend, sv4ScanBackend).forall { scan =>
191+
scan.httpHealth.successOption.exists(_.active)
192+
}
193+
allHealthy shouldBe true
194+
}
195+
196+
loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))(
197+
{
198+
aliceValidatorBackend.startSync()
199+
},
200+
logs => {
201+
val messages = logs.map(_.message)
202+
withClue("Validator should connect to sv1:") {
203+
connectionEstablished(1, messages)
204+
}
205+
withClue("Validator should connect to sv2:") {
206+
connectionEstablished(2, messages)
207+
}
208+
withClue("Validator should connect to sv3:") {
209+
connectionEstablished(3, messages)
210+
}
211+
withClue("Validator should NOT connect to sv4:") {
212+
connectionNotEstablished(4, messages)
213+
}
214+
},
215+
)
216+
217+
loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.INFO))(
218+
{
219+
sv1ScanBackend.stop()
220+
sv2ScanBackend.stop()
221+
},
222+
logs => {
223+
val messages = logs.map(_.message)
224+
withClue(
225+
"Validator should fail to reach consensus when trusted scans drop below the threshold:"
226+
) {
227+
messages.exists(
228+
_.contains(
229+
"Consensus not reached"
230+
)
231+
) should be(true)
232+
}
233+
},
234+
)
235+
236+
aliceValidatorBackend.stop()
237+
238+
}
239+
}

apps/common/src/main/scala/org/lfdecentralizedtrust/splice/config/Thresholds.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@ object Thresholds {
5959
val f = floor((svNum - 1) / 3.0).toInt
6060
PositiveInt.tryCreate(ceil((svNum + f + 1) / 2.0).toInt)
6161
}
62+
63+
def requiredNumScanThreshold(svNum: Int) = {
64+
FPlus1Threshold(svNum)
65+
}
6266
}

0 commit comments

Comments
 (0)