* fix(hyperliquid): derive native-tx routing labels from the ?hl= URL flag

The include_hl_native_tx/exclude_hl_native_tx detector classified a node by scanning the last 300 blocks for a system (native) topup tx from a per-chain address. That cannot work on testnet:

- the configured testnet address 0x6ed35e7d6de4b45f4efb8a91eff31afa49362569 was a regular bot (non-zero gasPrice, not filtered by hl-compliant mode, present on both node types);
- real testnet system txs (from 0x2222...) are far too sparse and bursty (median gap ~1200 blocks, max ~9000 = ~2.5h at ~1s/block) for any practical window;
- eth_getLogs is identical between compliant and non-compliant modes, so there is no cheap wide-range signal either.

Our hl-node upstreams already encode the mode in the URL (?hl=false serves native txs, ?hl=true is compliant). Read that flag directly:
- GenericUpstream captures the configured RPC/WS URL and exposes getRpcConnectionUrl();
- detectHlNativeTx emits include/exclude_hl_native_tx straight from ?hl= when present (cheap, exact, drift-free), for both mainnet and testnet;
- it falls back to the recent-blocks scan only when there is no ?hl= flag, and only on mainnet (testnet without the flag is too sparse to classify);
- the bogus HL_NATIVE_TX_FROM_TESTNET constant is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix hl

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vadim Filin
2026-06-05 13:41:41 +02:00
committed by GitHub
parent 2ddd9c8889
commit 634291bc98
4 changed files with 31 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.ChainResponse
import io.emeraldpay.dshackle.upstream.NodeTypeRequest
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@@ -15,7 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger
const val ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
const val HL_NATIVE_TX_FROM_MAINNET = "0x2222222222222222222222222222222222222222"
const val HL_NATIVE_TX_FROM_TESTNET = "0x6ed35e7d6de4b45f4efb8a91eff31afa49362569"
class EthereumUpstreamSettingsDetector(
private val _upstream: Upstream,
@@ -179,15 +179,17 @@ class EthereumUpstreamSettingsDetector(
Some clients on hyperliquid don't include system topup transactions, set either one of labels
*/
private fun detectHlNativeTx(): Flux<Pair<String, String>> {
// Only run HL native tx detection on Hyperliquid chains
if (chain != Chain.HYPERLIQUID__MAINNET && chain != Chain.HYPERLIQUID__TESTNET) {
return Flux.empty()
}
val hlNativeTxFrom = when (chain) {
Chain.HYPERLIQUID__MAINNET -> HL_NATIVE_TX_FROM_MAINNET
Chain.HYPERLIQUID__TESTNET -> HL_NATIVE_TX_FROM_TESTNET
else -> return Flux.empty()
// prefer the explicit ?hl= flag from the upstream URL
hlNativeTxLabelsFromUrl()?.let { return Flux.fromIterable(it) }
// no ?hl= flag: block-scan fallback (reliable only on mainnet)
if (chain != Chain.HYPERLIQUID__MAINNET) {
return Flux.empty()
}
val hlNativeTxFrom = HL_NATIVE_TX_FROM_MAINNET
if (detectCounter.get() % 5 != 1) {
return Flux.empty() // reduce frequency of detection
}
@@ -255,6 +257,19 @@ class EthereumUpstreamSettingsDetector(
}
}
// maps the upstream URL's ?hl= flag to routing labels, or null if absent
private fun hlNativeTxLabelsFromUrl(): List<Pair<String, String>>? {
val url = (upstream as? GenericUpstream)?.getRpcConnectionUrl()?.toString() ?: return null
// ?hl=false => serves native txs (include); ?hl=true => hl-node compliant (exclude)
return when {
url.contains(Regex("[?&]hl=false\\b")) ->
listOf("include_hl_native_tx" to "true", "exclude_hl_native_tx" to "false")
url.contains(Regex("[?&]hl=true\\b")) ->
listOf("exclude_hl_native_tx" to "true", "include_hl_native_tx" to "false")
else -> null
}
}
private fun detectArchiveNode(notArchived: Boolean): Mono<Pair<String, String>> {
if (notArchived) {
return Mono.empty()

View File

@@ -43,6 +43,7 @@ import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import reactor.core.scheduler.Schedulers
import java.net.URI
import java.time.Duration
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
@@ -101,6 +102,8 @@ open class GenericUpstream(
) {
rpcMethodsDetector = upstreamRpcMethodsDetectorBuilder(this, config)
detectRpcMethods(config, buildMethods)
rpcConnectionUrl = (config.connection as? UpstreamsConfig.RpcConnection)
?.let { it.rpc?.url ?: it.ws?.url }
}
private val validator: UpstreamValidator? = validatorBuilder(chain, this, getOptions(), chainConfig, versionRules)
@@ -126,6 +129,9 @@ open class GenericUpstream(
private val settingsDetector = upstreamSettingsDetectorBuilder(chain, this)
private var rpcMethodsDetector: UpstreamRpcMethodsDetector? = null
// configured RPC/WS URL (carries query flags like ?hl=)
private var rpcConnectionUrl: URI? = null
private val lowerBoundService = lowerBoundServiceBuilder(chain, this)
private val started = AtomicBoolean(false)
@@ -184,6 +190,8 @@ open class GenericUpstream(
)
}
fun getRpcConnectionUrl(): URI? = rpcConnectionUrl
@Suppress("UNCHECKED_CAST")
override fun <T : Upstream> cast(selfType: Class<T>): T {
if (!selfType.isAssignableFrom(this.javaClass)) {