Skip to content

Commit

Permalink
Check gas price (#494)
Browse files Browse the repository at this point in the history
* add gas validation option

* gas price config for chains

* check gas price

* add tests

* fix txt

* make gas price dls

* fix config
  • Loading branch information
tonatoz authored May 31, 2024
1 parent 4aff359 commit 772142e
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 19 deletions.
5 changes: 5 additions & 0 deletions docs/reference-configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,11 @@ If the Upstream is in _syncing_ state then the Dshackle doesn't use it for call
| Enable/Disable the call limit validation. Size of call limit is defined by `call-limit` option.
If it's enabled configuration parameter for chain `call-limit-contract` is required.

| `validate-gas-price`
| boolean
| `true`
| Enable/Disable the gas price validation. If it's enabled, the Dshackle will check the gas price of the upstream and will not use it if it's too high.

| `call-limit-size`
| number
| `1000000`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ChainOptions {
val minPeers: Int,
val validateSyncing: Boolean,
val validateCallLimit: Boolean,
val validateGasPrice: Boolean,
val validateChain: Boolean,
val callLimitSize: Int,
)
Expand All @@ -30,6 +31,7 @@ class ChainOptions {
var providesBalance: Boolean? = null,
var validatePeers: Boolean? = null,
var validateCallLimit: Boolean? = null,
var validateGasPrice: Boolean? = null,
var minPeers: Int? = null,
var validateSyncing: Boolean? = null,
var validateChain: Boolean? = null,
Expand All @@ -56,6 +58,7 @@ class ChainOptions {
copy.providesBalance = overwrites.providesBalance ?: this.providesBalance
copy.validateSyncing = overwrites.validateSyncing ?: this.validateSyncing
copy.validateCallLimit = overwrites.validateCallLimit ?: this.validateCallLimit
copy.validateGasPrice = overwrites.validateGasPrice ?: this.validateGasPrice
copy.timeout = overwrites.timeout ?: this.timeout
copy.validateChain = overwrites.validateChain ?: this.validateChain
copy.disableUpstreamValidation =
Expand All @@ -75,6 +78,7 @@ class ChainOptions {
this.minPeers ?: 1,
this.validateSyncing ?: true,
this.validateCallLimit ?: true,
this.validateGasPrice ?: true,
this.validateChain ?: true,
this.callLimitSize ?: 1_000_000,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class ChainOptionsReader : YamlConfigReader<ChainOptions.PartialOptions>() {
getValueAsBool(values, "validate-call-limit")?.let {
options.validateCallLimit = it
}
getValueAsBool(values, "validate-gas-price")?.let {
options.validateGasPrice = it
}
getValueAsBool(values, "validate-chain")?.let {
options.validateChain = it
}
Expand Down
32 changes: 27 additions & 5 deletions foundation/src/main/kotlin/org/drpc/chainsconfig/ChainsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,29 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
override fun iterator(): Iterator<ChainConfig> {
return chains.iterator()
}

companion object {
@JvmStatic
fun default(): ChainsConfig = ChainsConfig(emptyList())
}

class GasPriceCondition(private val condition: String) {
fun check(value: Long): Boolean {
val (op, valueStr) = condition.split(" ")
return when (op) {
"ne" -> value != valueStr.toLong()
"eq" -> value == valueStr.toLong()
"gt" -> value > valueStr.toLong()
"lt" -> value < valueStr.toLong()
"ge" -> value >= valueStr.toLong()
"le" -> value <= valueStr.toLong()
else -> throw IllegalArgumentException("Unsupported condition: $condition")
}
}

fun rules() = condition
}

data class ChainConfig(
val expectedBlockTime: Duration,
val syncingLagSize: Int,
Expand All @@ -30,7 +48,8 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
val callLimitContract: String?,
val id: String,
val blockchain: String,
val type: String
val type: String,
val gasPriceCondition: GasPriceCondition? = null,
) {
companion object {
@JvmStatic
Expand All @@ -50,12 +69,15 @@ data class ChainsConfig(private val chains: List<ChainConfig>) : Iterable<Chains
callLimitContract,
"undefined",
"undefined",
"unknown"
"unknown",
null,
)
}



@JvmStatic
fun defaultWithGasPriceCondition(gasPriceCondition: String) = defaultWithContract(null).copy(
gasPriceCondition = GasPriceCondition(gasPriceCondition),
)
}
}

fun resolve(chain: String): ChainConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class ChainsConfigReader(
?: throw IllegalArgumentException("undefined shortnames for $blockchain")
val type = getValueAsString(node, "type")
?: throw IllegalArgumentException("undefined type for $blockchain")
val gasPriceCondition = getValueAsString(node, "gas-price-condition")
return ChainsConfig.ChainConfig(
expectedBlockTime = expectedBlockTime,
syncingLagSize = lags.first,
Expand All @@ -99,7 +100,8 @@ class ChainsConfigReader(
shortNames = shortNames,
id = id,
blockchain = blockchain,
type = type
type = type,
gasPriceCondition = gasPriceCondition?.let { ChainsConfig.GasPriceCondition(gasPriceCondition) },
)
}

Expand Down
2 changes: 2 additions & 0 deletions foundation/src/main/resources/chains.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ chain-settings:
grpcId: 1006
chain-id: 0x38
short-names: [bsc, binance, bnb-smart-chain]
gas-price-condition: ne 3000000000
- id: Testnet
priority: 1
code: BSC_TESTNET
Expand Down Expand Up @@ -609,6 +610,7 @@ chain-settings:
short-names: [kava]
chain-id: 0x8ae
grpcId: 1025
gas-price-condition: eq 1000000000
- id: Testnet
priority: 10
code: KAVA_TESTNET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
validateChain(),
validateOldBlocks(),
validateCallLimit(),
validateGasPrice(),
).map {
listOf(it.t1, it.t2, it.t3).maxOf { it }
listOf(it.t1, it.t2, it.t3, it.t4).maxOf { it }
}.block() ?: ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR
}

Expand Down Expand Up @@ -195,6 +196,31 @@ open class EthereumUpstreamValidator @JvmOverloads constructor(
}
}

private fun validateGasPrice(): Mono<ValidateUpstreamSettingsResult> {
if (!options.validateGasPrice || config.gasPriceCondition == null) {
return Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_VALID)
}
return upstream.getIngressReader()
.read(ChainRequest("eth_gasPrice", ListParams()))
.flatMap(ChainResponse::requireStringResult)
.map { result ->
val actualGasPrice = result.substring(2).toLong(16)
if (!config.gasPriceCondition!!.check(actualGasPrice)) {
log.warn(
"Node ${upstream.getId()} has gasPrice $actualGasPrice, " +
"but it is not equal to the required ${config.gasPriceCondition!!.rules()}",
)
ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR
} else {
ValidateUpstreamSettingsResult.UPSTREAM_VALID
}
}
.onErrorResume { err ->
log.warn("Error during gasPrice validation", err)
Mono.just(ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR)
}
}

private fun chainId(): Mono<String> {
return upstream.getIngressReader()
.read(ChainRequest("eth_chainId", ListParams()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ class UpstreamsConfigReaderSpec extends Specification {
def options = partialOptions.buildOptions()
then:
options == new ChainOptions.Options(
false, false, 30, Duration.ofSeconds(60), null, true, 1, true, true, true, 1_000_000
false, false, 30, Duration.ofSeconds(60), null, true, 1, true, true, true, true, 1_000_000
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import spock.lang.Specification

import java.time.Duration

import static io.emeraldpay.dshackle.Chain.BSC__MAINNET
import static io.emeraldpay.dshackle.Chain.ETHEREUM__MAINNET
import static io.emeraldpay.dshackle.Chain.OPTIMISM__MAINNET
import static io.emeraldpay.dshackle.upstream.UpstreamAvailability.*
Expand All @@ -55,16 +56,16 @@ class EthereumUpstreamValidatorSpec extends Specification {
expect:
validator.resolve(Tuples.of(sync, peers)) == exp
where:
exp | sync | peers
OK | OK | OK
IMMATURE | OK | IMMATURE
UNAVAILABLE | OK | UNAVAILABLE
SYNCING | SYNCING | OK
SYNCING | SYNCING | IMMATURE
UNAVAILABLE | SYNCING | UNAVAILABLE
UNAVAILABLE | UNAVAILABLE | OK
UNAVAILABLE | UNAVAILABLE | IMMATURE
UNAVAILABLE | UNAVAILABLE | UNAVAILABLE
exp | sync | peers
OK | OK | OK
IMMATURE | OK | IMMATURE
UNAVAILABLE | OK | UNAVAILABLE
SYNCING | SYNCING | OK
SYNCING | SYNCING | IMMATURE
UNAVAILABLE | SYNCING | UNAVAILABLE
UNAVAILABLE | UNAVAILABLE | OK
UNAVAILABLE | UNAVAILABLE | IMMATURE
UNAVAILABLE | UNAVAILABLE | UNAVAILABLE
}

def "Doesnt check eth_syncing when disabled"() {
Expand Down Expand Up @@ -112,7 +113,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
Mono.just(new ChainResponse('false'.getBytes(), null))
]
}
2 * getHead() >> Mock(Head) {head ->
2 * getHead() >> Mock(Head) { head ->
1 * head.onSyncingNode(true)
1 * head.onSyncingNode(false)
}
Expand Down Expand Up @@ -276,6 +277,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateCallLimit = false
it.validateChain = false
it.validateGasPrice = false
}.buildOptions()
def up = Mock(Upstream) {
2 * getIngressReader() >>
Expand All @@ -297,6 +299,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
setup:
def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateChain = false
it.validateGasPrice = false
}.buildOptions()
def up = Mock(Upstream) {
3 * getIngressReader() >> Mock(Reader) {
Expand All @@ -321,6 +324,7 @@ class EthereumUpstreamValidatorSpec extends Specification {
setup:
def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateChain = false
it.validateGasPrice = false
}.buildOptions()
def up = Mock(Upstream) {
3 * getIngressReader() >> Mock(Reader) {
Expand All @@ -341,6 +345,52 @@ class EthereumUpstreamValidatorSpec extends Specification {
act == UPSTREAM_SETTINGS_ERROR
}

def "Upstream is valid if gas price is equal to expected"() {
setup:
def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateChain = false
it.validateCallLimit = false
}.buildOptions()
def conf = ChainConfig.defaultWithGasPriceCondition("ne 3000000000")
def up = Mock(Upstream) {
3 * getIngressReader() >>
Mock(Reader) {
1 * read(new ChainRequest("eth_gasPrice", new ListParams())) >> Mono.just(new ChainResponse('"0x3b9aca00"'.getBytes(), null))
1 * read(new ChainRequest("eth_blockNumber", new ListParams())) >> Mono.just(new ChainResponse('"0x10ff9be"'.getBytes(), null))
1 * read(new ChainRequest("eth_getBlockByNumber", new ListParams(["0x10fd2ae", false]))) >>
Mono.just(new ChainResponse('"result"'.getBytes(), null))
}
}
def validator = new EthereumUpstreamValidator(BSC__MAINNET, up, options, conf)
when:
def act = validator.validateUpstreamSettingsOnStartup()
then:
act == UPSTREAM_VALID
}

def "Upstream is NOT valid if gas price is different from expected"() {
setup:
def options = ChainOptions.PartialOptions.getDefaults().tap {
it.validateChain = false
it.validateCallLimit = false
}.buildOptions()
def conf = ChainConfig.defaultWithGasPriceCondition("eq 1000000000")
def up = Mock(Upstream) {
3 * getIngressReader() >>
Mock(Reader) {
1 * read(new ChainRequest("eth_gasPrice", new ListParams())) >> Mono.just(new ChainResponse('"0xb2d05e00"'.getBytes(), null))
1 * read(new ChainRequest("eth_blockNumber", new ListParams())) >> Mono.just(new ChainResponse('"0x10ff9be"'.getBytes(), null))
1 * read(new ChainRequest("eth_getBlockByNumber", new ListParams(["0x10fd2ae", false]))) >>
Mono.just(new ChainResponse('"result"'.getBytes(), null))
}
}
def validator = new EthereumUpstreamValidator(BSC__MAINNET, up, options, conf)
when:
def act = validator.validateUpstreamSettingsOnStartup()
then:
act == UPSTREAM_FATAL_SETTINGS_ERROR
}

def "Upstream is valid if chain settings are valid"() {
setup:
def options = ChainOptions.PartialOptions.getDefaults().tap {
Expand Down

0 comments on commit 772142e

Please sign in to comment.