From 4d4a2ed655c4b7a59168bf33e3cf628eace6301e Mon Sep 17 00:00:00 2001 From: Artem Rootman <4586640+artemrootman@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:04:05 +0200 Subject: [PATCH] add aztec networks (#785) * add aztec networks * fix linter errors & checkout from main (public) * fix AztecLowerBoundService --- .../kotlin/chainsconfig.codegen.gradle.kts | 1 + emerald-grpc | 2 +- .../io/emeraldpay/dshackle/BlockchainType.kt | 3 +- foundation/src/main/resources/public | 2 +- .../dshackle/upstream/CallTargetsHolder.kt | 3 + .../upstream/aztec/AztecChainSpecific.kt | 177 ++++++++++++++++++ .../upstream/aztec/AztecLowerBoundService.kt | 15 ++ .../aztec/AztecLowerBoundStateDetector.kt | 24 +++ .../upstream/calls/DefaultAztecMethods.kt | 94 ++++++++++ .../upstream/generic/ChainSpecific.kt | 3 + 10 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecChainSpecific.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundService.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundStateDetector.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultAztecMethods.kt diff --git a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts index 445d89cc..40d5f3a4 100644 --- a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts @@ -128,6 +128,7 @@ open class CodeGen(private val config: ChainsConfig) { private fun type(type: String): String { return when(type) { + "aztec" -> "BlockchainType.AZTEC" "eth" -> "BlockchainType.ETHEREUM" "bitcoin" -> "BlockchainType.BITCOIN" "starknet" -> "BlockchainType.STARKNET" diff --git a/emerald-grpc b/emerald-grpc index 0986b4b9..38db8b3f 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit 0986b4b9db80f84e61121b1b0c7dd17b6824fa13 +Subproject commit 38db8b3fefb81f105c64a61ce0b2027c654066c6 diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt index 1f10d9bc..227f6dde 100644 --- a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt @@ -5,6 +5,7 @@ enum class BlockchainType( ) { UNKNOWN(ApiType.JSON_RPC), BITCOIN(ApiType.JSON_RPC), + AZTEC(ApiType.JSON_RPC), ETHEREUM(ApiType.JSON_RPC), STARKNET(ApiType.JSON_RPC), POLKADOT(ApiType.JSON_RPC), @@ -19,4 +20,4 @@ enum class BlockchainType( enum class ApiType { JSON_RPC, REST; -} \ No newline at end of file +} diff --git a/foundation/src/main/resources/public b/foundation/src/main/resources/public index 23fcd398..91a3853a 160000 --- a/foundation/src/main/resources/public +++ b/foundation/src/main/resources/public @@ -1 +1 @@ -Subproject commit 23fcd39890ef1fd2ff0fdb73021361043d513feb +Subproject commit 91a3853af9b88b951674cd872644225108ec5868 diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt index 6e5e05c4..fa51101b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt @@ -1,5 +1,6 @@ package io.emeraldpay.dshackle.upstream +import io.emeraldpay.dshackle.BlockchainType.AZTEC import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.COSMOS import io.emeraldpay.dshackle.BlockchainType.ETHEREUM @@ -16,6 +17,7 @@ import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.calls.DefaultAztecMethods import io.emeraldpay.dshackle.upstream.calls.DefaultBeaconChainMethods import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.DefaultCosmosMethods @@ -47,6 +49,7 @@ class CallTargetsHolder { ): CallMethods { val created = when (chain.type) { BITCOIN -> DefaultBitcoinMethods(options.providesBalance == true) + AZTEC -> DefaultAztecMethods() ETHEREUM -> DefaultEthereumMethods(chain) STARKNET -> DefaultStarknetMethods(chain) POLKADOT -> DefaultPolkadotMethods(chain) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecChainSpecific.kt new file mode 100644 index 00000000..10b3a253 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecChainSpecific.kt @@ -0,0 +1,177 @@ +package io.emeraldpay.dshackle.upstream.aztec + +import com.fasterxml.jackson.databind.JsonNode +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.foundation.ChainOptions.Options +import io.emeraldpay.dshackle.reader.ChainReader +import io.emeraldpay.dshackle.upstream.ChainRequest +import io.emeraldpay.dshackle.upstream.GenericSingleCallValidator +import io.emeraldpay.dshackle.upstream.SingleValidator +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.UpstreamAvailability +import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult +import io.emeraldpay.dshackle.upstream.generic.AbstractPollChainSpecific +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService +import io.emeraldpay.dshackle.upstream.rpcclient.ListParams +import org.slf4j.LoggerFactory +import reactor.core.publisher.Mono +import java.math.BigInteger +import java.time.Instant + +object AztecChainSpecific : AbstractPollChainSpecific() { + private val log = LoggerFactory.getLogger(AztecChainSpecific::class.java) + + override fun parseBlock(data: ByteArray, upstreamId: String, api: ChainReader): Mono { + val root = Global.objectMapper.readTree(data) + val height = parseLong( + findNode( + root, + "number", + "header.number", + "blockNumber", + "header.blockNumber", + "header.globalVariables.blockNumber", + ), + ) ?: 0L + val hashValue = parseText(findNode(root, "hash", "header.hash", "blockHash", "header.blockHash")) + val parentValue = parseText( + findNode( + root, + "parentHash", + "header.parentHash", + "parent_hash", + "header.parent_hash", + "prevHash", + "header.prevHash", + ), + ) + val timestamp = parseInstant( + findNode( + root, + "timestamp", + "header.timestamp", + "header.globalVariables.timestamp", + ), + ) ?: Instant.EPOCH + + if (hashValue == null) { + log.warn("Aztec block hash is missing in response from upstream {}", upstreamId) + } + + return Mono.just( + BlockContainer( + height = height, + hash = BlockId.from(hashValue ?: "0x0"), + difficulty = BigInteger.ZERO, + timestamp = timestamp, + full = false, + json = data, + parsed = root, + transactions = emptyList(), + upstreamId = upstreamId, + parentHash = parentValue?.let { BlockId.from(it) }, + ), + ) + } + + override fun getFromHeader(data: ByteArray, upstreamId: String, api: ChainReader): Mono { + throw NotImplementedError() + } + + override fun listenNewHeadsRequest(): ChainRequest { + throw NotImplementedError() + } + + override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest { + throw NotImplementedError() + } + + override fun upstreamValidators( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): List> { + return listOf( + GenericSingleCallValidator( + ChainRequest("node_isReady", ListParams()), + upstream, + ) { data -> + val raw = Global.objectMapper.readTree(data) + val ready = when { + raw.isBoolean -> raw.asBoolean() + raw.isTextual -> raw.asText().equals("true", ignoreCase = true) + else -> raw.asBoolean(false) + } + if (ready) UpstreamAvailability.OK else UpstreamAvailability.SYNCING + }, + ) + } + + override fun upstreamSettingsValidators( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): List> { + return emptyList() + } + + override fun lowerBoundService(chain: Chain, upstream: Upstream): LowerBoundService { + return AztecLowerBoundService(chain, upstream) + } + + override fun latestBlockRequest(): ChainRequest = + ChainRequest("node_getBlock", ListParams("latest")) + + private fun findNode(root: JsonNode, vararg paths: String): JsonNode? { + for (path in paths) { + var current: JsonNode? = root + for (part in path.split(".")) { + current = current?.get(part) + if (current == null || current.isMissingNode) { + break + } + } + if (current != null && !current.isMissingNode && !current.isNull) { + return current + } + } + return null + } + + private fun parseText(node: JsonNode?): String? { + if (node == null || node.isNull || node.isMissingNode) { + return null + } + return node.asText().ifBlank { null } + } + + private fun parseLong(node: JsonNode?): Long? { + if (node == null || node.isNull || node.isMissingNode) { + return null + } + return when { + node.isNumber -> node.asLong() + node.isTextual -> parseNumericString(node.asText()) + else -> null + } + } + + private fun parseNumericString(value: String): Long? { + val trimmed = value.trim() + if (trimmed.isEmpty()) return null + val isHex = trimmed.startsWith("0x") || trimmed.startsWith("0X") + val raw = if (isHex) trimmed.substring(2) else trimmed + return runCatching { BigInteger(raw, if (isHex) 16 else 10).toLong() }.getOrNull() + } + + private fun parseInstant(node: JsonNode?): Instant? { + val ts = parseLong(node) ?: return null + return if (ts >= 1_000_000_000_000L) Instant.ofEpochMilli(ts) else Instant.ofEpochSecond(ts) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundService.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundService.kt new file mode 100644 index 00000000..69f327aa --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundService.kt @@ -0,0 +1,15 @@ +package io.emeraldpay.dshackle.upstream.aztec + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundService + +class AztecLowerBoundService( + chain: Chain, + private val upstream: Upstream, +) : LowerBoundService(chain, upstream) { + override fun detectors(): List { + return listOf(AztecLowerBoundStateDetector(upstream.getChain())) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundStateDetector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundStateDetector.kt new file mode 100644 index 00000000..fc29f1ab --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/aztec/AztecLowerBoundStateDetector.kt @@ -0,0 +1,24 @@ +package io.emeraldpay.dshackle.upstream.aztec + +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundData +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundDetector +import io.emeraldpay.dshackle.upstream.lowerbound.LowerBoundType +import reactor.core.publisher.Flux + +class AztecLowerBoundStateDetector( + chain: Chain, +) : LowerBoundDetector(chain) { + + override fun period(): Long { + return 120 + } + + override fun internalDetectLowerBound(): Flux { + return Flux.just(LowerBoundData(1, LowerBoundType.STATE)) + } + + override fun types(): Set { + return setOf(LowerBoundType.STATE) + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultAztecMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultAztecMethods.kt new file mode 100644 index 00000000..ede7c902 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultAztecMethods.kt @@ -0,0 +1,94 @@ +package io.emeraldpay.dshackle.upstream.calls + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcException + +class DefaultAztecMethods : CallMethods { + + private val broadcast = setOf( + "node_sendTx", + ) + + private val allowedMethods: Set = setOf( + "node_getBlockNumber", + "node_getProvenBlockNumber", + "node_getL2Tips", + "node_getBlock", + "node_getBlocks", + "node_getBlockHeader", + "node_sendTx", + "node_getTxReceipt", + "node_getTxEffect", + "node_getTxByHash", + "node_getPendingTxs", + "node_getPendingTxCount", + "node_isValidTx", + "node_simulatePublicCalls", + "node_getPublicStorageAt", + "node_getWorldStateSyncStatus", + "node_findLeavesIndexes", + "node_getNullifierSiblingPath", + "node_getNoteHashSiblingPath", + "node_getArchiveSiblingPath", + "node_getPublicDataSiblingPath", + "node_getNullifierMembershipWitness", + "node_getLowNullifierMembershipWitness", + "node_getPublicDataWitness", + "node_getArchiveMembershipWitness", + "node_getNoteHashMembershipWitness", + "node_getL1ToL2MessageMembershipWitness", + "node_getL1ToL2MessageBlock", + "node_isL1ToL2MessageSynced", + "node_getL2ToL1Messages", + "node_getPrivateLogs", + "node_getPublicLogs", + "node_getContractClassLogs", + "node_getLogsByTags", + "node_getContractClass", + "node_getContract", + "node_isReady", + "node_getNodeInfo", + "node_getNodeVersion", + "node_getVersion", + "node_getChainId", + "node_getL1ContractAddresses", + "node_getProtocolContractAddresses", + "node_getEncodedEnr", + "node_getCurrentBaseFees", + "node_getValidatorsStats", + "node_getValidatorStats", + "node_registerContractFunctionSignatures", + "node_getAllowedPublicSetup", + ) + + override fun createQuorumFor(method: String): CallQuorum { + return when { + broadcast.contains(method) -> BroadcastQuorum() + else -> AlwaysQuorum() + } + } + + override fun isCallable(method: String): Boolean { + return allowedMethods.contains(method) + } + + override fun isHardcoded(method: String): Boolean { + return false + } + + override fun executeHardcoded(method: String): ByteArray { + throw RpcException(-32601, "Method not found") + } + + override fun getGroupMethods(groupName: String): Set = + when (groupName) { + "default" -> getSupportedMethods() + else -> emptySet() + } + + override fun getSupportedMethods(): Set { + return allowedMethods.toSortedSet() + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt index d60ef4c5..ddb6863f 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt @@ -1,5 +1,6 @@ package io.emeraldpay.dshackle.upstream.generic +import io.emeraldpay.dshackle.BlockchainType.AZTEC import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.COSMOS import io.emeraldpay.dshackle.BlockchainType.ETHEREUM @@ -32,6 +33,7 @@ import io.emeraldpay.dshackle.upstream.UpstreamRpcMethodsDetector import io.emeraldpay.dshackle.upstream.UpstreamSettingsDetector import io.emeraldpay.dshackle.upstream.UpstreamValidator import io.emeraldpay.dshackle.upstream.ValidateUpstreamSettingsResult +import io.emeraldpay.dshackle.upstream.aztec.AztecChainSpecific import io.emeraldpay.dshackle.upstream.beaconchain.BeaconChainSpecific import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallSelector @@ -113,6 +115,7 @@ object ChainSpecificRegistry { @JvmStatic fun resolve(chain: Chain): ChainSpecific { return when (chain.type) { + AZTEC -> AztecChainSpecific ETHEREUM -> EthereumChainSpecific STARKNET -> StarknetChainSpecific POLKADOT -> PolkadotChainSpecific