Skip to content

Commit f9f42be

Browse files
committed
staterecover: adds blob scan and el client
1 parent 253ef18 commit f9f42be

File tree

14 files changed

+865
-0
lines changed

14 files changed

+865
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
plugins {
2+
id 'net.consensys.zkevm.kotlin-library-conventions'
3+
}
4+
5+
group = 'build.linea.staterecover'
6+
7+
dependencies {
8+
api(project(':jvm-libs:generic:extensions:kotlin'))
9+
api(project(':jvm-libs:linea:core:domain-models'))
10+
api(project(':state-recover:appcore:domain-models'))
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package build.linea.staterecover.clients
2+
3+
import tech.pegasys.teku.infrastructure.async.SafeFuture
4+
5+
interface BlobFetcher {
6+
fun fetchBlobsByHash(blobVersionedHashes: List<ByteArray>): SafeFuture<List<ByteArray>>
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package build.linea.staterecover.clients
2+
3+
import build.linea.staterecover.BlockL1RecoveredData
4+
import net.consensys.linea.BlockNumberAndHash
5+
import net.consensys.linea.BlockParameter
6+
import tech.pegasys.teku.infrastructure.async.SafeFuture
7+
8+
interface ExecutionLayerClient {
9+
fun getBlockNumberAndHash(blockParameter: BlockParameter): SafeFuture<BlockNumberAndHash>
10+
fun lineaEngineImportBlocksFromBlob(blocks: List<BlockL1RecoveredData>): SafeFuture<Unit>
11+
fun lineaEngineForkChoiceUpdated(headBlockHash: ByteArray, finalizedBlockHash: ByteArray): SafeFuture<Unit>
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
plugins {
2+
id 'net.consensys.zkevm.kotlin-library-conventions'
3+
}
4+
5+
group = 'build.linea.staterecover'
6+
7+
dependencies {
8+
api(project(':jvm-libs:generic:extensions:kotlin'))
9+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package build.linea.staterecover
2+
3+
import kotlinx.datetime.Instant
4+
import net.consensys.encodeHex
5+
6+
data class BlockExtraData(
7+
val beneficiary: ByteArray
8+
) {
9+
override fun equals(other: Any?): Boolean {
10+
if (this === other) return true
11+
if (javaClass != other?.javaClass) return false
12+
13+
other as BlockExtraData
14+
15+
return beneficiary.contentEquals(other.beneficiary)
16+
}
17+
18+
override fun hashCode(): Int {
19+
return beneficiary.contentHashCode()
20+
}
21+
22+
override fun toString(): String {
23+
return "BlockExtraData(beneficiary=${beneficiary.encodeHex()})"
24+
}
25+
}
26+
27+
data class BlockL1RecoveredData(
28+
val blockNumber: ULong,
29+
val blockHash: ByteArray,
30+
val coinbase: ByteArray,
31+
val blockTimestamp: Instant,
32+
val gasLimit: ULong,
33+
val difficulty: ULong,
34+
val extraData: BlockExtraData,
35+
val transactions: List<TransactionL1RecoveredData>
36+
) {
37+
override fun equals(other: Any?): Boolean {
38+
if (this === other) return true
39+
if (javaClass != other?.javaClass) return false
40+
41+
other as BlockL1RecoveredData
42+
43+
if (blockNumber != other.blockNumber) return false
44+
if (!blockHash.contentEquals(other.blockHash)) return false
45+
if (!coinbase.contentEquals(other.coinbase)) return false
46+
if (blockTimestamp != other.blockTimestamp) return false
47+
if (gasLimit != other.gasLimit) return false
48+
if (difficulty != other.difficulty) return false
49+
if (extraData != other.extraData) return false
50+
if (transactions != other.transactions) return false
51+
52+
return true
53+
}
54+
55+
override fun hashCode(): Int {
56+
var result = blockNumber.hashCode()
57+
result = 31 * result + blockHash.contentHashCode()
58+
result = 31 * result + coinbase.contentHashCode()
59+
result = 31 * result + blockTimestamp.hashCode()
60+
result = 31 * result + gasLimit.hashCode()
61+
result = 31 * result + difficulty.hashCode()
62+
result = 31 * result + extraData.hashCode()
63+
result = 31 * result + transactions.hashCode()
64+
return result
65+
}
66+
67+
override fun toString(): String {
68+
return "BlockL1RecoveredData(" +
69+
"blockNumber=$blockNumber, " +
70+
"blockHash=${blockHash.encodeHex()}, " +
71+
"coinbase=${coinbase.encodeHex()}, " +
72+
"blockTimestamp=$blockTimestamp, " +
73+
"gasLimit=$gasLimit, " +
74+
"difficulty=$difficulty, " +
75+
"extraData=$extraData, " +
76+
"transactions=$transactions)"
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package build.linea.staterecover
2+
3+
import java.math.BigInteger
4+
5+
data class TransactionL1RecoveredData(
6+
val type: UByte,
7+
val nonce: ULong,
8+
val maxPriorityFeePerGas: BigInteger,
9+
val maxFeePerGas: BigInteger,
10+
val gasLimit: ULong,
11+
val from: ByteArray,
12+
val to: ByteArray,
13+
val value: BigInteger,
14+
val data: ByteArray,
15+
val accessList: List<AccessTuple>
16+
) {
17+
18+
data class AccessTuple(
19+
val address: ByteArray,
20+
val storageKeys: List<ByteArray>
21+
) {
22+
override fun equals(other: Any?): Boolean {
23+
if (this === other) return true
24+
if (javaClass != other?.javaClass) return false
25+
26+
other as AccessTuple
27+
28+
if (!address.contentEquals(other.address)) return false
29+
if (storageKeys != other.storageKeys) return false
30+
31+
return true
32+
}
33+
34+
override fun hashCode(): Int {
35+
var result = address.contentHashCode()
36+
result = 31 * result + storageKeys.hashCode()
37+
return result
38+
}
39+
}
40+
41+
override fun equals(other: Any?): Boolean {
42+
if (this === other) return true
43+
if (javaClass != other?.javaClass) return false
44+
45+
other as TransactionL1RecoveredData
46+
47+
if (type != other.type) return false
48+
if (nonce != other.nonce) return false
49+
if (maxPriorityFeePerGas != other.maxPriorityFeePerGas) return false
50+
if (maxFeePerGas != other.maxFeePerGas) return false
51+
if (gasLimit != other.gasLimit) return false
52+
if (!from.contentEquals(other.from)) return false
53+
if (!to.contentEquals(other.to)) return false
54+
if (value != other.value) return false
55+
if (!data.contentEquals(other.data)) return false
56+
if (accessList != other.accessList) return false
57+
58+
return true
59+
}
60+
61+
override fun hashCode(): Int {
62+
var result = type.hashCode()
63+
result = 31 * result + nonce.hashCode()
64+
result = 31 * result + maxPriorityFeePerGas.hashCode()
65+
result = 31 * result + maxFeePerGas.hashCode()
66+
result = 31 * result + gasLimit.hashCode()
67+
result = 31 * result + from.contentHashCode()
68+
result = 31 * result + to.contentHashCode()
69+
result = 31 * result + value.hashCode()
70+
result = 31 * result + data.contentHashCode()
71+
result = 31 * result + accessList.hashCode()
72+
return result
73+
}
74+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
2+
import org.gradle.api.tasks.testing.logging.TestLogEvent
3+
4+
plugins {
5+
id 'net.consensys.zkevm.kotlin-library-conventions'
6+
}
7+
8+
group = 'build.linea.staterecover'
9+
10+
dependencies {
11+
implementation(project(':jvm-libs:generic:extensions:futures'))
12+
implementation(project(':jvm-libs:generic:extensions:kotlin'))
13+
implementation(project(':jvm-libs:generic:extensions:tuweni'))
14+
implementation(project(':jvm-libs:generic:http-rest'))
15+
implementation(project(':jvm-libs:generic:json-rpc'))
16+
implementation(project(':jvm-libs:generic:vertx-helper'))
17+
implementation(project(':jvm-libs:linea:clients:linea-state-manager'))
18+
implementation(project(':jvm-libs:linea:core:domain-models'))
19+
implementation(project(':jvm-libs:linea:core:long-running-service'))
20+
implementation(project(':state-recover:appcore:clients-interfaces'))
21+
implementation("io.vertx:vertx-web-client:${libs.versions.vertx}")
22+
23+
testImplementation "com.github.tomakehurst:wiremock-jre8:${libs.versions.wiremock.get()}"
24+
testImplementation "org.slf4j:slf4j-api:1.7.30"
25+
testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:${libs.versions.log4j}"
26+
testImplementation "org.apache.logging.log4j:log4j-core:${libs.versions.log4j}"
27+
}
28+
29+
sourceSets {
30+
integrationTest {
31+
kotlin {
32+
compileClasspath += sourceSets.main.output
33+
runtimeClasspath += sourceSets.main.output
34+
}
35+
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + sourceSets.test.compileClasspath
36+
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath
37+
}
38+
}
39+
40+
task integrationTest(type: Test) {
41+
test ->
42+
description = "Runs integration tests."
43+
group = "verification"
44+
useJUnitPlatform()
45+
46+
classpath = sourceSets.integrationTest.runtimeClasspath
47+
testClassesDirs = sourceSets.integrationTest.output.classesDirs
48+
49+
dependsOn(":localStackComposeUp")
50+
dependsOn(rootProject.tasks.compileContracts)
51+
52+
testLogging {
53+
events TestLogEvent.FAILED,
54+
TestLogEvent.SKIPPED,
55+
TestLogEvent.STANDARD_ERROR,
56+
TestLogEvent.STARTED,
57+
TestLogEvent.PASSED
58+
exceptionFormat TestExceptionFormat.FULL
59+
showCauses true
60+
showExceptions true
61+
showStackTraces true
62+
// set showStandardStreams if you need to see test logs
63+
showStandardStreams false
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package build.linea.staterecover.clients.blobscan
2+
3+
import build.linea.staterecover.clients.BlobFetcher
4+
import io.vertx.core.Vertx
5+
import io.vertx.core.json.JsonObject
6+
import io.vertx.ext.web.client.WebClient
7+
import io.vertx.ext.web.client.WebClientOptions
8+
import net.consensys.decodeHex
9+
import net.consensys.encodeHex
10+
import net.consensys.linea.jsonrpc.client.RequestRetryConfig
11+
import net.consensys.linea.vertx.setDefaultsFrom
12+
import org.apache.logging.log4j.LogManager
13+
import org.apache.logging.log4j.Logger
14+
import tech.pegasys.teku.infrastructure.async.SafeFuture
15+
import java.net.URI
16+
17+
class BlobScanClient(
18+
private val restClient: RestClient<JsonObject>,
19+
private val log: Logger = LogManager.getLogger(BlobScanClient::class.java)
20+
) : BlobFetcher {
21+
fun getBlobById(id: String): SafeFuture<ByteArray> {
22+
return restClient
23+
.get("/blobs/$id")
24+
.thenApply { response ->
25+
if (response.statusCode == 200) {
26+
response.body!!.getString("data").decodeHex()
27+
} else {
28+
throw RuntimeException(
29+
"error fetching blobId=$id " +
30+
"errorMessage=${response.body?.getString("message") ?: ""}"
31+
)
32+
}
33+
}
34+
}
35+
36+
override fun fetchBlobsByHash(blobVersionedHashes: List<ByteArray>): SafeFuture<List<ByteArray>> {
37+
return SafeFuture.collectAll(blobVersionedHashes.map { hash -> getBlobById(hash.encodeHex()) }.stream())
38+
}
39+
40+
companion object {
41+
fun create(
42+
vertx: Vertx,
43+
endpoint: URI,
44+
requestRetryConfig: RequestRetryConfig
45+
): BlobScanClient {
46+
val restClient = VertxRestClient(
47+
vertx = vertx,
48+
webClient = WebClient.create(vertx, WebClientOptions().setDefaultsFrom(endpoint)),
49+
responseParser = { it.toJsonObject() },
50+
retryableErrorCodes = setOf(429, 503, 504),
51+
requestRetryConfig = requestRetryConfig
52+
)
53+
return BlobScanClient(restClient)
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package build.linea.staterecover.clients.blobscan
2+
3+
import io.vertx.core.Vertx
4+
import io.vertx.core.buffer.Buffer
5+
import io.vertx.ext.web.client.HttpRequest
6+
import io.vertx.ext.web.client.HttpResponse
7+
import io.vertx.ext.web.client.WebClient
8+
import net.consensys.linea.async.AsyncRetryer
9+
import net.consensys.linea.async.toSafeFuture
10+
import net.consensys.linea.jsonrpc.client.RequestRetryConfig
11+
import tech.pegasys.teku.infrastructure.async.SafeFuture
12+
13+
// TODO: move to a common module
14+
data class RestResponse<T>(
15+
val statusCode: Int,
16+
val body: T?
17+
)
18+
19+
interface RestClient<Response> {
20+
fun get(path: String): SafeFuture<RestResponse<Response>>
21+
// add remaining verbs as we need them
22+
}
23+
24+
class VertxRestClient<Response>(
25+
private val vertx: Vertx,
26+
private val webClient: WebClient,
27+
private val responseParser: (Buffer) -> Response,
28+
private val retryableErrorCodes: Set<Int> = DEFAULT_RETRY_HTTP_CODES,
29+
private val requestRetryConfig: RequestRetryConfig,
30+
private val asyncRetryer: AsyncRetryer<HttpResponse<Buffer>> = AsyncRetryer.retryer(
31+
backoffDelay = requestRetryConfig.backoffDelay,
32+
maxRetries = requestRetryConfig.maxRetries?.toInt(),
33+
timeout = requestRetryConfig.timeout,
34+
vertx = vertx
35+
),
36+
private val requestHeaders: Map<String, String> = mapOf("Accept" to "application/json")
37+
) : RestClient<Response> {
38+
private fun makeRequestWithRetry(
39+
request: HttpRequest<Buffer>
40+
): SafeFuture<HttpResponse<Buffer>> {
41+
return asyncRetryer
42+
.retry(
43+
stopRetriesPredicate = { response: HttpResponse<Buffer> ->
44+
response.statusCode() !in retryableErrorCodes
45+
}
46+
) {
47+
request.send().toSafeFuture()
48+
}
49+
}
50+
51+
override fun get(path: String): SafeFuture<RestResponse<Response>> {
52+
return makeRequestWithRetry(
53+
webClient
54+
.get(path)
55+
.apply { requestHeaders.forEach(::putHeader) }
56+
)
57+
.thenApply { response ->
58+
val parsedResponse = response.body()?.let(responseParser)
59+
RestResponse(response.statusCode(), parsedResponse)
60+
}
61+
}
62+
63+
companion object {
64+
val DEFAULT_RETRY_HTTP_CODES = setOf(429, 500, 503, 504)
65+
}
66+
}

0 commit comments

Comments
 (0)