Skip to content

Commit be0fb67

Browse files
authored
Making Maru work without an L2 EL node (#451)
* Making Maru work without an L2 EL node * Added MaruFollowerNoElTest test and some more validations * Stricten access to payloadValidationEnabled for QBFT Validator * Fixed Protocol starter start up, other minor fixes * Added some more config tests * Addressed most of the comments * Added a fallback to linea section * Minor improvements * Trying out setup helm v4.3.1 * Fixed a typo * Simplified the online provider * Fixed the validation * Spotless * Reverted the change in MaruFollowerNoElTest * WIP * Changed the base values in the test * Fixed the fork-transition configs in the chaos tests * Fixed the fork-transition configs in the chaos tests * Fixed allowEmptyBlocks propagation in 1 place * Reverted modified timestampSeconds * Made MaruAppFactoryCheckEthApiTest more concise
1 parent fb02931 commit be0fb67

File tree

22 files changed

+917
-230
lines changed

22 files changed

+917
-230
lines changed

.github/workflows/chaos-testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
- name: Setup Gradle
7171
uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # 4.4.0
7272
- name: Install helm
73-
uses: azure/[email protected].0
73+
uses: azure/[email protected].1
7474
with:
7575
version: 'v3.18.3'
7676
id: install

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
integrationTestImplementation(group: 'org.hyperledger.besu.internal', name: 'besu-ethereum-core', classifier: 'test-support')
5757
integrationTestImplementation("org.hyperledger.besu.internal:besu-ethereum-p2p")
5858
integrationTestImplementation("org.hyperledger.besu.internal:besu-consensus-qbft-core")
59+
testImplementation(testFixtures(project(":core")))
5960
}
6061

6162
// Besu and Teku have similar versioning and some modules are also named the same. This creates conflicts when
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright Consensys Software Inc.
3+
*
4+
* This file is dual-licensed under either the MIT license or Apache License 2.0.
5+
* See the LICENSE-MIT and LICENSE-APACHE files in the repository root for details.
6+
*
7+
* SPDX-License-Identifier: MIT OR Apache-2.0
8+
*/
9+
package maru.app
10+
11+
import com.fasterxml.jackson.databind.JsonNode
12+
import com.fasterxml.jackson.databind.ObjectMapper
13+
import java.io.File
14+
import java.net.URI
15+
import java.net.http.HttpClient
16+
import java.net.http.HttpRequest
17+
import java.net.http.HttpResponse
18+
import kotlin.time.Duration.Companion.seconds
19+
import kotlin.time.toJavaDuration
20+
import org.apache.logging.log4j.LogManager
21+
import org.assertj.core.api.Assertions.assertThat
22+
import org.awaitility.kotlin.await
23+
import org.hyperledger.besu.tests.acceptance.dsl.blockchain.Amount
24+
import org.hyperledger.besu.tests.acceptance.dsl.condition.net.NetConditions
25+
import org.hyperledger.besu.tests.acceptance.dsl.node.ThreadBesuNodeRunner
26+
import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.Cluster
27+
import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.ClusterConfigurationBuilder
28+
import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetTransactions
29+
import org.junit.jupiter.api.AfterEach
30+
import org.junit.jupiter.api.BeforeEach
31+
import org.junit.jupiter.api.Test
32+
import org.junit.jupiter.api.io.TempDir
33+
import testutils.SingleNodeNetworkStack
34+
import testutils.besu.BesuTransactionsHelper
35+
import testutils.maru.MaruFactory
36+
import testutils.maru.awaitTillMaruHasPeers
37+
38+
class MaruFollowerNoElTest {
39+
private lateinit var cluster: Cluster
40+
private lateinit var validatorStack: SingleNodeNetworkStack
41+
42+
@TempDir
43+
private lateinit var maruFollowerDataDir: File
44+
private lateinit var maruFollower: MaruApp
45+
private lateinit var transactionsHelper: BesuTransactionsHelper
46+
private val log = LogManager.getLogger(this.javaClass)
47+
private val maruFactory = MaruFactory()
48+
49+
private var validatorApiPort: UInt = 0u
50+
private var followerApiPort: UInt = 0u
51+
52+
@BeforeEach
53+
fun setUp() {
54+
transactionsHelper = BesuTransactionsHelper()
55+
cluster =
56+
Cluster(
57+
ClusterConfigurationBuilder().build(),
58+
NetConditions(NetTransactions()),
59+
ThreadBesuNodeRunner(),
60+
)
61+
62+
validatorStack =
63+
SingleNodeNetworkStack(
64+
cluster = cluster,
65+
) { ethereumJsonRpcBaseUrl, engineRpcUrl, tmpDir ->
66+
maruFactory.buildTestMaruValidatorWithP2pPeering(
67+
ethereumJsonRpcUrl = ethereumJsonRpcBaseUrl,
68+
engineApiRpc = engineRpcUrl,
69+
dataDir = tmpDir,
70+
apiPort = 0u,
71+
startApiServer = true,
72+
)
73+
}
74+
75+
// Start all Besu nodes together for proper peering
76+
validatorStack.maruApp.start()
77+
78+
// Discover actual validator API port
79+
validatorApiPort = validatorStack.maruApp.apiPort()
80+
81+
// Get the validator's p2p port after it's started
82+
val validatorP2pPort = validatorStack.p2pPort
83+
84+
maruFollower =
85+
maruFactory.buildTestMaruFollowerWithP2pPeering(
86+
ethereumJsonRpcUrl = null,
87+
engineApiRpc = null,
88+
dataDir = maruFollowerDataDir.toPath(),
89+
validatorPortForStaticPeering = validatorP2pPort,
90+
syncingConfig = MaruFactory.defaultSyncingConfig,
91+
enablePayloadValidation = false,
92+
apiPort = 0u,
93+
startApiServer = true,
94+
)
95+
96+
maruFollower.start()
97+
98+
// Discover actual follower API port
99+
followerApiPort = maruFollower.apiPort()
100+
101+
log.info("Nodes are peered")
102+
maruFollower.awaitTillMaruHasPeers(1u)
103+
validatorStack.maruApp.awaitTillMaruHasPeers(1u)
104+
}
105+
106+
@AfterEach
107+
fun tearDown() {
108+
maruFollower.stop()
109+
validatorStack.maruApp.stop()
110+
validatorStack.maruApp.close()
111+
cluster.close()
112+
}
113+
114+
// TODO: Replace with a proper Beacon REST API client
115+
private val httpClient: HttpClient = HttpClient.newHttpClient()
116+
private val objectMapper = ObjectMapper()
117+
118+
private data class ClBlockMetadata(
119+
val slot: ULong,
120+
val blockHash: String?,
121+
)
122+
123+
private fun readHead(apiPort: UInt): ClBlockMetadata {
124+
val req =
125+
HttpRequest
126+
.newBuilder(URI.create("http://127.0.0.1:${apiPort.toInt()}/eth/v2/beacon/blocks/head"))
127+
.GET()
128+
.build()
129+
val resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString())
130+
require(resp.statusCode() == 200) { "Unexpected status ${resp.statusCode()}: ${resp.body()}" }
131+
val root: JsonNode = objectMapper.readTree(resp.body())
132+
val message =
133+
root
134+
.path("data")
135+
.path("message")
136+
val slotStr =
137+
message
138+
.path("slot")
139+
.asText()
140+
require(slotStr.isNotBlank()) { "slot not found in response: ${resp.body()}" }
141+
val slot = slotStr.toULong()
142+
143+
val blockHashStr =
144+
message
145+
.path("body")
146+
.path("attestations")
147+
.find { it.path("data").path("slot").asInt() == slot.toInt() }
148+
?.path("data")
149+
?.path("body_root")
150+
?.asText()
151+
152+
return ClBlockMetadata(slot, blockHashStr)
153+
}
154+
155+
@Test
156+
fun `Maru follower is able to import blocks without EL`() {
157+
val blocksToProduce = 4 // Less than desync tolerance
158+
159+
val initialFollowerHead = readHead(followerApiPort)
160+
val initialValidatorHead = readHead(validatorApiPort)
161+
162+
repeat(blocksToProduce) {
163+
transactionsHelper.run {
164+
validatorStack.besuNode.sendTransactionAndAssertExecution(
165+
logger = log,
166+
recipient = createAccount("another account"),
167+
amount = Amount.ether(100),
168+
)
169+
}
170+
}
171+
172+
// Await until both validator and follower advanced and heads match
173+
await
174+
.pollInterval(1.seconds.toJavaDuration())
175+
.timeout(20.seconds.toJavaDuration())
176+
.untilAsserted {
177+
val validatorHead = readHead(validatorApiPort)
178+
val followerHead = readHead(followerApiPort)
179+
180+
assertThat(validatorHead.blockHash).isNotNull
181+
assertThat(validatorHead.slot).isGreaterThanOrEqualTo(initialValidatorHead.slot + blocksToProduce.toULong())
182+
assertThat(followerHead.slot).isGreaterThanOrEqualTo(initialFollowerHead.slot + blocksToProduce.toULong())
183+
assertThat(followerHead).isEqualTo(validatorHead)
184+
}
185+
}
186+
}

app/src/main/kotlin/maru/app/MaruApp.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import net.consensys.linea.vertx.ObservabilityServer
3838
import org.apache.logging.log4j.LogManager
3939
import org.apache.logging.log4j.Logger
4040
import org.hyperledger.besu.plugin.services.MetricsSystem
41+
import org.web3j.protocol.Web3j
4142
import tech.pegasys.teku.ethereum.executionclient.web3j.Web3JClient
4243

4344
class MaruApp(
@@ -52,8 +53,8 @@ class MaruApp(
5253
private val vertx: Vertx,
5354
private val metricsFacade: MetricsFacade,
5455
private val metricsSystem: MetricsSystem,
55-
private val validatorELNodeEthJsonRpcClient: Web3JClient,
56-
private val validatorELNodeEngineApiWeb3JClient: Web3JClient,
56+
private val l2EthWeb3j: Web3j?,
57+
private val validatorELNodeEngineApiWeb3JClient: Web3JClient?,
5758
private val apiServer: ApiServer,
5859
private val syncStatusProvider: SyncStatusProvider,
5960
private val syncControllerManager: LongRunningService,
@@ -95,6 +96,8 @@ class MaruApp(
9596

9697
fun p2pPort(): UInt = p2pNetwork.port
9798

99+
fun apiPort(): UInt = apiServer.port().toUInt()
100+
98101
private val nextTargetBlockTimestampProvider =
99102
NextBlockTimestampProviderImpl(
100103
clock = clock,
@@ -142,8 +145,8 @@ class MaruApp(
142145
}
143146

144147
override fun close() {
145-
validatorELNodeEngineApiWeb3JClient.eth1Web3j.shutdown()
146-
validatorELNodeEthJsonRpcClient.eth1Web3j.shutdown()
148+
validatorELNodeEngineApiWeb3JClient?.eth1Web3j?.shutdown()
149+
l2EthWeb3j?.shutdown()
147150
followerELNodeEngineApiWeb3JClients.forEach { (_, web3jClient) -> web3jClient.eth1Web3j.shutdown() }
148151
p2pNetwork.close()
149152
vertx.close()
@@ -185,7 +188,7 @@ class MaruApp(
185188
QbftProtocolValidatorFactory(
186189
qbftOptions = config.qbft!!,
187190
privateKeyBytes = privateKeyWithoutPrefix,
188-
validatorELNodeEngineApiWeb3JClient = validatorELNodeEngineApiWeb3JClient,
191+
validatorELNodeEngineApiWeb3JClient = validatorELNodeEngineApiWeb3JClient!!,
189192
followerELNodeEngineApiWeb3JClients = followerELNodeEngineApiWeb3JClients,
190193
metricsSystem = metricsSystem,
191194
finalizationStateProvider = finalizationProvider,
@@ -197,7 +200,7 @@ class MaruApp(
197200
allowEmptyBlocks = config.allowEmptyBlocks,
198201
syncStatusProvider = syncStatusProvider,
199202
forksSchedule = beaconGenesisConfig,
200-
payloadValidationEnabled = config.validatorElNode.payloadValidationEnabled,
203+
payloadValidationEnabled = config.validatorElNode!!.payloadValidationEnabled,
201204
)
202205
} else {
203206
QbftFollowerFactory(
@@ -208,14 +211,14 @@ class MaruApp(
208211
metricsFacade = metricsFacade,
209212
allowEmptyBlocks = config.allowEmptyBlocks,
210213
finalizationStateProvider = finalizationProvider,
211-
payloadValidationEnabled = config.validatorElNode.payloadValidationEnabled,
214+
payloadValidationEnabled = config.validatorElNode?.payloadValidationEnabled ?: false,
212215
)
213216
}
214217
val forkTransitionSubscriptionManager = InOrderFanoutSubscriptionManager<ForkSpec>()
215218
forkTransitionSubscriptionManager.addSyncSubscriber(p2pNetwork::handleForkTransition)
216219
val difficultyAwareQbftFactory =
217220
DifficultyAwareQbftFactory(
218-
ethereumJsonRpcClient = validatorELNodeEthJsonRpcClient.eth1Web3j,
221+
ethereumJsonRpcClient = l2EthWeb3j,
219222
postTtdProtocolFactory = qbftFactory,
220223
)
221224
val protocolStarter =
@@ -228,7 +231,7 @@ class MaruApp(
228231
),
229232
nextBlockTimestampProvider = nextTargetBlockTimestampProvider,
230233
syncStatusProvider = syncStatusProvider,
231-
forkTransitionCheckInterval = config.protocolTransitionPollingInterval,
234+
forkTransitionCheckInterval = config.forkTransition.protocolTransitionPollingInterval,
232235
forkTransitionNotifier = forkTransitionSubscriptionManager,
233236
clock = clock,
234237
)

0 commit comments

Comments
 (0)