Merge pull request #73 from p2p-org/revert-69-sync-upstream
Revert "Sync upstream 7.12.2022"
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
<html>
|
||||
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"
|
||||
type="application/javascript">
|
||||
</script>
|
||||
|
||||
<script type="application/javascript">
|
||||
provider = new ethers.providers.WebSocketProvider("ws://127.0.0.1:9080/eth");
|
||||
provider.getBlockNumber().then((blockNumber) => {
|
||||
console.log("from dshackle", blockNumber);
|
||||
}).catch(console.warn);
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -678,8 +678,7 @@ configuration, and may be omitted for most of the situations.
|
||||
|
||||
| `role`
|
||||
| no
|
||||
| `primary` (default), `secondary` or `fallback`.
|
||||
First it makes the requests to the upstreams with role `primary`, then if none are available to upstreams with role `secondary`.
|
||||
| `standard` (default) or `fallback`.
|
||||
Fallback role mean that the upstream is used only after other upstreams failed or didn't return quorum
|
||||
|
||||
| `chain`
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.rpc.NativeSubscribe
|
||||
import io.emeraldpay.etherjar.rpc.json.RequestJson
|
||||
import io.emeraldpay.etherjar.rpc.json.ResponseJson
|
||||
import io.netty.buffer.ByteBufInputStream
|
||||
import io.netty.buffer.Unpooled
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -73,8 +74,9 @@ class WebsocketHandler(
|
||||
val eventHandler = accessHandler.start(req, routeConfig.blockchain)
|
||||
|
||||
val responses = respond(routeConfig.blockchain, control, requests, eventHandler)
|
||||
.map { Unpooled.wrappedBuffer(it.toByteArray()) }
|
||||
|
||||
resp.sendString(responses, Charsets.UTF_8)
|
||||
resp.send(responses)
|
||||
.then()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,11 @@ open class NativeCall(
|
||||
Mono.just(ctx).flatMap(this::executeOnRemote)
|
||||
)
|
||||
.onErrorResume {
|
||||
Mono.just(CallResult.fail(ctx.id, ctx.nonce, it))
|
||||
if (it is CallFailure) {
|
||||
Mono.just(CallResult.fail(it.id, ctx.nonce, it.reason))
|
||||
} else {
|
||||
Mono.just(CallResult.fail(ctx.id, ctx.nonce, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,14 +312,21 @@ open class NativeCall(
|
||||
CallResult.ok(ctx.id, ctx.nonce, bytes, it.signature, ctx.upstream.getId())
|
||||
}
|
||||
.onErrorResume { t ->
|
||||
Mono.just(CallResult.fail(ctx.id, ctx.nonce, t))
|
||||
val failure = when (t) {
|
||||
is CallFailure -> CallResult.fail(t.id, ctx.nonce, t.reason)
|
||||
is JsonRpcException -> CallResult.fail(ctx.id, ctx.nonce, t.error.code, t.error.message)
|
||||
else -> CallResult.fail(ctx.id, ctx.nonce, t)
|
||||
}
|
||||
Mono.just(failure)
|
||||
}
|
||||
.switchIfEmpty(
|
||||
Mono.fromSupplier {
|
||||
counter.get().let { attempts ->
|
||||
CallResult.fail(
|
||||
ctx.id, ctx.nonce,
|
||||
CallError(1, "No response or no available upstream for ${ctx.payload.method}", null)
|
||||
ctx.id,
|
||||
ctx.nonce,
|
||||
1,
|
||||
errorMessage(attempts, ctx.payload.method)
|
||||
).also {
|
||||
countFailure(attempts, ctx)
|
||||
}
|
||||
@@ -480,15 +491,7 @@ open class NativeCall(
|
||||
is JsonRpcException -> CallError(t.id.asNumber().toInt(), t.error.message, t.error)
|
||||
is RpcException -> CallError(t.code, t.rpcMessage, null)
|
||||
is CallFailure -> CallError(t.id, t.reason.message ?: "Upstream Error", null)
|
||||
else -> {
|
||||
// May only happen if it's an unhandled exception.
|
||||
// In this case try to find a meaningless details in the stack. Most important reason for doing that is to find an ID of the request
|
||||
if (t.cause != null) {
|
||||
from(t.cause!!)
|
||||
} else {
|
||||
CallError(1, t.message ?: "Upstream Error", null)
|
||||
}
|
||||
}
|
||||
else -> CallError(1, t.message ?: "Upstream Error", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -507,8 +510,8 @@ open class NativeCall(
|
||||
return CallResult(id, nonce, result, null, signature, upstreamId)
|
||||
}
|
||||
|
||||
fun fail(id: Int, nonce: Long?, error: CallError): CallResult {
|
||||
return CallResult(id, nonce, null, error, null, null)
|
||||
fun fail(id: Int, nonce: Long?, errorCore: Int, errorMessage: String): CallResult {
|
||||
return CallResult(id, nonce, null, CallError(errorCore, errorMessage, null), null, null)
|
||||
}
|
||||
|
||||
fun fail(id: Int, nonce: Long?, error: Throwable): CallResult {
|
||||
|
||||
@@ -98,11 +98,6 @@ abstract class DefaultUpstream(
|
||||
}
|
||||
|
||||
fun statusByLag(lag: Long, proposed: UpstreamAvailability): UpstreamAvailability {
|
||||
if (options.disableValidation == true) {
|
||||
// if we specifically told that this upstream should be _always valid_ then skip
|
||||
// the status calculation and trust the proposed value as is
|
||||
return proposed
|
||||
}
|
||||
return if (proposed == UpstreamAvailability.OK) {
|
||||
when {
|
||||
lag > 6 -> UpstreamAvailability.SYNCING
|
||||
|
||||
@@ -405,8 +405,7 @@ open class WsConnection(
|
||||
)
|
||||
|
||||
val response = Flux.from(rpcReceive.asFlux())
|
||||
// send the request _after_ WS subscribes to the responses, otherwise the response may come before the actual subscription and be lost
|
||||
.doOnRequest { sendRpc(request) }
|
||||
.doOnSubscribe { sendRpc(request) }
|
||||
.filter { resp -> resp.id.asNumber() == expectedId }
|
||||
.take(Defaults.timeout)
|
||||
.take(1)
|
||||
|
||||
@@ -25,8 +25,6 @@ import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.grpc.StatusRuntimeException
|
||||
import org.apache.commons.lang3.time.StopWatch
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -34,7 +32,7 @@ import java.util.concurrent.TimeUnit
|
||||
class JsonRpcGrpcClient(
|
||||
private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
private val chain: Chain,
|
||||
private val metrics: RpcMetrics?,
|
||||
private val metrics: RpcMetrics
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -48,11 +46,11 @@ class JsonRpcGrpcClient(
|
||||
class Executor(
|
||||
private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
private val chain: Chain,
|
||||
private val metrics: RpcMetrics?
|
||||
private val metrics: RpcMetrics
|
||||
) : Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
|
||||
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||
val timer = StopWatch()
|
||||
var startTime: Long = 0
|
||||
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChainValue(chain.id)
|
||||
|
||||
@@ -68,58 +66,39 @@ class JsonRpcGrpcClient(
|
||||
req.addItems(reqItem.build())
|
||||
|
||||
return Mono.just(key)
|
||||
.doOnNext { timer.start() }
|
||||
.flatMap {
|
||||
.doOnNext {
|
||||
startTime = System.nanoTime()
|
||||
}.flatMap {
|
||||
stub.nativeCall(req.build())
|
||||
.single()
|
||||
.onErrorResume(::handleError)
|
||||
.flatMap(::handleResponse)
|
||||
.flatMap { resp ->
|
||||
if (resp.succeed) {
|
||||
val bytes = resp.payload.toByteArray()
|
||||
val signature = if (resp.hasSignature()) {
|
||||
extractSignature(resp.signature)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Mono.just(JsonRpcResponse(bytes, null, JsonRpcResponse.NumberId(0), signature))
|
||||
} else {
|
||||
metrics.fails.increment()
|
||||
Mono.error(
|
||||
RpcException(
|
||||
RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR,
|
||||
resp.errorMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
if (timer.isStarted) {
|
||||
metrics?.timer?.record(timer.getTime(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS)
|
||||
if (startTime > 0) {
|
||||
val now = System.nanoTime()
|
||||
metrics.timer.record(now - startTime, TimeUnit.NANOSECONDS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleResponse(resp: BlockchainOuterClass.NativeCallReplyItem): Mono<JsonRpcResponse> =
|
||||
if (resp.succeed) {
|
||||
val bytes = resp.payload.toByteArray()
|
||||
val signature = if (resp.hasSignature()) {
|
||||
extractSignature(resp.signature)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Mono.just(JsonRpcResponse(bytes, null, JsonRpcResponse.NumberId(0), signature))
|
||||
} else {
|
||||
metrics?.fails?.increment()
|
||||
Mono.error(
|
||||
RpcException(
|
||||
RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR,
|
||||
resp.errorMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleError(t: Throwable): Mono<BlockchainOuterClass.NativeCallReplyItem> {
|
||||
metrics?.fails?.increment()
|
||||
return when (t) {
|
||||
is StatusRuntimeException -> Mono.error(
|
||||
RpcException(
|
||||
RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR,
|
||||
"Remote status code: ${t.status.code.name}"
|
||||
)
|
||||
)
|
||||
|
||||
else -> Mono.error(
|
||||
RpcException(
|
||||
RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR,
|
||||
"Other connection error"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun extractSignature(resp: NativeCallReplySignature?): ResponseSigner.Signature? {
|
||||
if (resp == null || resp.signature == null || resp.signature.isEmpty || resp.upstreamId == null || resp.upstreamId.isEmpty()) {
|
||||
return null
|
||||
|
||||
@@ -24,13 +24,10 @@ import io.netty.handler.codec.http.HttpHeaderNames
|
||||
import io.netty.handler.codec.http.HttpHeaders
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import io.netty.resolver.DefaultAddressResolverGroup
|
||||
import org.apache.commons.lang3.time.StopWatch
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.netty.http.client.HttpClient
|
||||
import reactor.netty.resources.ConnectionProvider
|
||||
import reactor.util.function.Tuple2
|
||||
import reactor.util.function.Tuples
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertificateFactory
|
||||
@@ -38,7 +35,6 @@ import java.security.cert.X509Certificate
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
* JSON RPC client
|
||||
@@ -93,95 +89,52 @@ class JsonRpcHttpClient(
|
||||
this.httpClient = build
|
||||
}
|
||||
|
||||
fun execute(request: ByteArray): Mono<Tuple2<Int, ByteArray>> {
|
||||
fun execute(request: ByteArray): Mono<ByteArray> {
|
||||
val response = httpClient
|
||||
.post()
|
||||
.uri(target)
|
||||
.send(Mono.just(request).map { Unpooled.wrappedBuffer(it) })
|
||||
|
||||
return response.response { header, bytes ->
|
||||
val statusCode = header.status().code()
|
||||
bytes.aggregate().asByteArray().map {
|
||||
Tuples.of(statusCode, it)
|
||||
if (header.status().code() != 200) {
|
||||
Mono.error(
|
||||
JsonRpcException(
|
||||
JsonRpcResponse.NumberId(-2),
|
||||
JsonRpcError(
|
||||
RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE,
|
||||
"HTTP Code: ${header.status().code()}"
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
bytes.aggregate().asByteArray()
|
||||
}
|
||||
}.single()
|
||||
}
|
||||
|
||||
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||
val startTime = StopWatch()
|
||||
var startTime: Long = 0
|
||||
return Mono.just(key)
|
||||
.map(JsonRpcRequest::toJson)
|
||||
.doOnNext { startTime.start() }
|
||||
.doOnNext {
|
||||
startTime = System.nanoTime()
|
||||
}
|
||||
.flatMap(this@JsonRpcHttpClient::execute)
|
||||
.doOnNext {
|
||||
if (startTime.isStarted) {
|
||||
metrics.timer.record(startTime.nanoTime, TimeUnit.NANOSECONDS)
|
||||
if (startTime > 0) {
|
||||
val now = System.nanoTime()
|
||||
metrics.timer.record(now - startTime, TimeUnit.NANOSECONDS)
|
||||
}
|
||||
}
|
||||
.transform(asJsonRpcResponse(key))
|
||||
.transform(convertErrors(key))
|
||||
.transform(throwIfError())
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscribers expect to catch an exception if the response contains JSON RPC Error. Convert it here to JsonRpcException
|
||||
*/
|
||||
private fun throwIfError(): Function<Mono<JsonRpcResponse>, Mono<JsonRpcResponse>> {
|
||||
return Function { resp ->
|
||||
resp.flatMap {
|
||||
if (it.hasError()) {
|
||||
Mono.error(JsonRpcException(it.id, it.error!!))
|
||||
} else {
|
||||
Mono.just(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert internal exceptions to standard JsonRpcException
|
||||
*/
|
||||
private fun convertErrors(key: JsonRpcRequest): Function<Mono<JsonRpcResponse>, Mono<JsonRpcResponse>> {
|
||||
return Function { resp ->
|
||||
resp.onErrorResume { t ->
|
||||
.map(parser::parse)
|
||||
.onErrorResume { t ->
|
||||
val err = when (t) {
|
||||
is RpcException -> JsonRpcException.from(t)
|
||||
is JsonRpcException -> t
|
||||
else -> JsonRpcException(key.id, t.message ?: t.javaClass.name)
|
||||
is RpcException -> JsonRpcResponse.error(t.code, t.rpcMessage)
|
||||
is JsonRpcException -> JsonRpcResponse.error(t.error, JsonRpcResponse.NumberId(1))
|
||||
else -> JsonRpcResponse.error(1, t.message ?: t.javaClass.name)
|
||||
}
|
||||
// here we're measure the internal errors, not upstream errors
|
||||
metrics.fails.increment()
|
||||
Mono.error(err)
|
||||
Mono.just(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process response from the upstream and convert it to JsonRpcResponse.
|
||||
* The input is a pair of (Http Status Code, Http Response Body)
|
||||
*/
|
||||
private fun asJsonRpcResponse(key: JsonRpcRequest): Function<Mono<Tuple2<Int, ByteArray>>, Mono<JsonRpcResponse>> {
|
||||
return Function { resp ->
|
||||
resp.map {
|
||||
val parsed = parser.parse(it.t2)
|
||||
val statusCode = it.t1
|
||||
if (statusCode != 200) {
|
||||
if (parsed.hasError() && parsed.error!!.code != RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE) {
|
||||
// extracted the error details from the HTTP Body
|
||||
parsed
|
||||
} else {
|
||||
// here we got a valid response with ERROR as HTTP Status Code. We assume that HTTP Status has
|
||||
// a higher priority so return an error here anyway
|
||||
JsonRpcResponse.error(
|
||||
RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE,
|
||||
"HTTP Code: $statusCode",
|
||||
JsonRpcResponse.NumberId(key.id)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
@@ -59,18 +58,15 @@ abstract class ResponseParser<T> {
|
||||
} catch (e: JsonParseException) {
|
||||
log.warn("Failed to parse JSON from upstream: ${e.message}")
|
||||
}
|
||||
return if (state.isReady) {
|
||||
state
|
||||
} else {
|
||||
log.debug("Failed to parse `${StringUtils.abbreviateMiddle(String(json), "...", 200)}` JSON")
|
||||
state.copy(
|
||||
result = null,
|
||||
error = JsonRpcError(
|
||||
RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE,
|
||||
"Invalid JSON structure: never finalized"
|
||||
)
|
||||
)
|
||||
if (state.isReady) {
|
||||
return state
|
||||
}
|
||||
return Preparsed(
|
||||
error = JsonRpcError(
|
||||
RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE,
|
||||
"Invalid JSON structure: never finalized"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
open fun process(parser: JsonParser, json: ByteArray, field: String, state: Preparsed): Preparsed {
|
||||
|
||||
@@ -44,16 +44,6 @@ class ResponseWSParser : ResponseParser<ResponseWSParser.WsResponse>() {
|
||||
state.error
|
||||
)
|
||||
}
|
||||
if (state.error != null) {
|
||||
return WsResponse(
|
||||
// we don't have any real option because it's just an invalid value and can be anything,
|
||||
// so let's suppose its Type as RPC as a most likely scenario
|
||||
Type.RPC,
|
||||
state.id ?: JsonRpcResponse.Id.from(0),
|
||||
null,
|
||||
state.error
|
||||
)
|
||||
}
|
||||
throw IllegalStateException("State is not ready")
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class CacheConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read full"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/cache-redis-full.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("cache-redis-full.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
@@ -39,7 +39,7 @@ class CacheConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read disabled"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/cache-redis-disabled.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("cache-redis-disabled.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class HealthConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read empty"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-health-empty.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-health-empty.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
@@ -38,7 +38,7 @@ class HealthConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read single"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-health-1.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-health-1.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
@@ -58,7 +58,7 @@ class HealthConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read multiple"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-health-2.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-health-2.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
|
||||
@@ -22,12 +22,11 @@ import spock.lang.Specification
|
||||
|
||||
class MainConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read full config with inclusion"() {
|
||||
MainConfigReader reader = new MainConfigReader(TestingCommons.fileResolver())
|
||||
|
||||
def "Read full config"() {
|
||||
setup:
|
||||
// the File Resolver should be able to resolve/include files
|
||||
MainConfigReader reader = new MainConfigReader(TestingCommons.fileResolver())
|
||||
// note that it references another config to be included
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-full.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-full.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class MonitoringConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read basic monitoring config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-monitoring-basic.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-monitoring-basic.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class ProxyConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read basic proxy config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-proxy-basic.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-basic.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
@@ -42,7 +42,7 @@ class ProxyConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read proxy config with websocket disabled"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-proxy-no-ws.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-no-ws.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
@@ -53,7 +53,7 @@ class ProxyConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read proxy config with two elements"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-proxy-two.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-two.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
@@ -73,7 +73,7 @@ class ProxyConfigReaderSpec extends Specification {
|
||||
|
||||
def "Read max proxy config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/dshackle-proxy-max.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-max.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse standard config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-basic.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-basic.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -73,7 +73,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse websocket-only config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-ws-only.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-ws-only.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -98,7 +98,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse full defined websocket config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-ws-full.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-ws-full.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -125,7 +125,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse bitcoin upstreams"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-bitcoin.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-bitcoin.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -173,7 +173,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse bitcoin upstreams with esplora"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-bitcoin-esplora.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-bitcoin-esplora.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -202,7 +202,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse ds config"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-ds.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-ds.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -225,7 +225,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with labels"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-labels.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-labels.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -246,7 +246,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with options"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-options.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-options.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -262,7 +262,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config without defaults"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-no-defaults.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-no-defaults.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -285,7 +285,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with methods"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-methods.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-methods.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -305,7 +305,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with methods and quorum"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-methods-quorum.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-methods-quorum.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -328,7 +328,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with invalid ids"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-no-id.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-no-id.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -355,7 +355,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config without fallback role"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-basic.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-basic.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -367,7 +367,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with fallback role"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-roles.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -379,7 +379,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with secondary role"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-roles-2.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles-2.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
@@ -392,7 +392,7 @@ class UpstreamsConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse config with invalid role"() {
|
||||
setup:
|
||||
def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-roles-invalid.yaml")
|
||||
def config = this.class.getClassLoader().getResourceAsStream("upstreams-roles-invalid.yaml")
|
||||
when:
|
||||
def act = reader.read(config)
|
||||
then:
|
||||
|
||||
@@ -34,8 +34,6 @@ import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
@@ -155,35 +153,6 @@ class NativeCallSpec extends Specification {
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Returns error details from remote"() {
|
||||
setup:
|
||||
def quorum = new AlwaysQuorum()
|
||||
|
||||
def nativeCall = nativeCall()
|
||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.error(
|
||||
new JsonRpcException(JsonRpcResponse.Id.from(12), new JsonRpcError(-32123, "Foo Bar", "Foo Bar Baz"))
|
||||
)
|
||||
}
|
||||
}
|
||||
def call = new NativeCall.ValidCallContext(12, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
when:
|
||||
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))
|
||||
then:
|
||||
resp.isError()
|
||||
with(resp.getError()) {
|
||||
message == "Foo Bar"
|
||||
upstreamError != null
|
||||
with (upstreamError) {
|
||||
code == -32123
|
||||
details == "Foo Bar Baz"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def "Packs call exception into response with id"() {
|
||||
setup:
|
||||
def nativeCall = nativeCall()
|
||||
|
||||
@@ -26,6 +26,14 @@ class MockGrpcServer {
|
||||
|
||||
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule()
|
||||
|
||||
ReactorBlockchainGrpc.ReactorBlockchainStub clientForServer(ReactorBlockchainGrpc.BlockchainImplBase impl) {
|
||||
String serverName = InProcessServerBuilder.generateName()
|
||||
grpcCleanup.register(InProcessServerBuilder
|
||||
.forName(serverName).directExecutor().addService(impl).build().start());
|
||||
def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build())
|
||||
return ReactorBlockchainGrpc.newReactorStub(channel)
|
||||
}
|
||||
|
||||
ReactorBlockchainGrpc.ReactorBlockchainStub clientForServer(BlockchainGrpc.BlockchainImplBase impl){
|
||||
String serverName = InProcessServerBuilder.generateName()
|
||||
grpcCleanup.register(InProcessServerBuilder
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestingCommons {
|
||||
}
|
||||
|
||||
static FileResolver fileResolver() {
|
||||
return new FileResolver(new File("src/test/resources/configs"))
|
||||
return new FileResolver(new File("src/test/resources"))
|
||||
}
|
||||
|
||||
static BlockContainer blockForEthereum(Long height) {
|
||||
|
||||
@@ -92,7 +92,7 @@ class WsConnectionSpec extends Specification {
|
||||
it.id.asNumber() == 15L && Global.objectMapper.readValue(it.result, TransactionJson) == tx
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
.verify(Duration.ofSeconds(5))
|
||||
}
|
||||
|
||||
def "Makes a RPC call - return null"() {
|
||||
@@ -115,7 +115,7 @@ class WsConnectionSpec extends Specification {
|
||||
it.resultAsRawString == 'null'
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
.verify(Duration.ofSeconds(5))
|
||||
}
|
||||
|
||||
def "Makes a RPC call - return error"() {
|
||||
@@ -140,6 +140,6 @@ class WsConnectionSpec extends Specification {
|
||||
it.error.code == RpcResponseError.CODE_METHOD_NOT_EXIST && it.error.message == "test"
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
.verify(Duration.ofSeconds(5))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package io.emeraldpay.dshackle.upstream.rpcclient
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainGrpc
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.test.MockGrpcServer
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.emeraldpay.dshackle.Chain
|
||||
import io.grpc.stub.StreamObserver
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class JsonRpcGrpcClientSpec extends Specification {
|
||||
|
||||
def "Makes a request"() {
|
||||
setup:
|
||||
def mockGrpc = new MockGrpcServer()
|
||||
def requested = new AtomicReference<BlockchainOuterClass.NativeCallRequest>()
|
||||
|
||||
def grpc = mockGrpc.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||
@Override
|
||||
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
||||
requested.set(request)
|
||||
responseObserver.onNext(BlockchainOuterClass.NativeCallReplyItem.newBuilder()
|
||||
.setId(1)
|
||||
.setSucceed(true)
|
||||
.setPayload(ByteString.copyFromUtf8("\"hello world!\""))
|
||||
.build())
|
||||
responseObserver.onCompleted()
|
||||
}
|
||||
})
|
||||
def client = new JsonRpcGrpcClient(
|
||||
grpc, Chain.BITCOIN, null
|
||||
).getReader()
|
||||
|
||||
when:
|
||||
def act = client.read(
|
||||
new JsonRpcRequest("test", [])
|
||||
).block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
!act.hasError()
|
||||
act.resultAsProcessedString == "hello world!"
|
||||
|
||||
requested.get() == BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.CHAIN_BITCOIN)
|
||||
.addAllItems([
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setId(1)
|
||||
.setMethod("test")
|
||||
.setPayload(ByteString.copyFromUtf8("[]"))
|
||||
.build()
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
def "Return error on HTTP error"() {
|
||||
setup:
|
||||
def mockGrpc = new MockGrpcServer()
|
||||
def grpc = mockGrpc.clientForServer(new BlockchainGrpc.BlockchainImplBase() {
|
||||
@Override
|
||||
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
|
||||
responseObserver.onError(new IllegalStateException("fail"))
|
||||
}
|
||||
})
|
||||
def client = new JsonRpcGrpcClient(
|
||||
grpc, Chain.BITCOIN, null
|
||||
).getReader()
|
||||
|
||||
when:
|
||||
client.read(
|
||||
new JsonRpcRequest("test", [])
|
||||
).block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
def t = thrown(RpcException)
|
||||
with(t.error) {
|
||||
message == "Remote status code: UNKNOWN"
|
||||
it.code == RpcResponseError.CODE_UPSTREAM_CONNECTION_ERROR
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import org.mockserver.model.HttpRequest
|
||||
import org.mockserver.model.HttpResponse
|
||||
import org.mockserver.model.MediaType
|
||||
import org.springframework.util.SocketUtils
|
||||
import reactor.core.Exceptions
|
||||
import reactor.test.StepVerifier
|
||||
import spock.lang.Specification
|
||||
|
||||
@@ -85,7 +84,7 @@ class JsonRpcHttpClientSpec extends Specification {
|
||||
.withBody("pong")
|
||||
)
|
||||
when:
|
||||
def act = client.execute("ping".bytes).map { new String(it.t2) }
|
||||
def act = client.execute("ping".bytes).map { new String(it) }
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNext("pong")
|
||||
@@ -112,44 +111,13 @@ class JsonRpcHttpClientSpec extends Specification {
|
||||
.withBody("pong")
|
||||
)
|
||||
when:
|
||||
def act = client.read(
|
||||
new JsonRpcRequest("ping", [])
|
||||
).block(Duration.ofSeconds(1))
|
||||
def act = client.execute("ping".bytes).map { new String(it) }
|
||||
then:
|
||||
def t = thrown(RuntimeException) // reactor.core.Exceptions$ReactiveException
|
||||
t.cause instanceof JsonRpcException
|
||||
with(((JsonRpcException)t.cause).error) {
|
||||
code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE
|
||||
message == "HTTP Code: 500"
|
||||
}
|
||||
}
|
||||
|
||||
def "Tries to extract message if HTTP error if it still contains a JSON RPC message"() {
|
||||
setup:
|
||||
def client = new JsonRpcHttpClient("localhost:${port}", metrics, null, null)
|
||||
|
||||
mockServer.when(
|
||||
HttpRequest.request()
|
||||
).respond(
|
||||
HttpResponse.response()
|
||||
.withStatusCode(500)
|
||||
.withBody('{' +
|
||||
'"jsonrpc": "2.0", ' +
|
||||
'"id": 1, ' +
|
||||
'"error": {"code": -32603, "message": "Something happened"}' +
|
||||
'}')
|
||||
)
|
||||
when:
|
||||
def act = client.read(
|
||||
new JsonRpcRequest("ping", [])
|
||||
).block(Duration.ofSeconds(1))
|
||||
then:
|
||||
def t = thrown(RuntimeException) // reactor.core.Exceptions$ReactiveException
|
||||
t.cause instanceof JsonRpcException
|
||||
with(((JsonRpcException)t.cause).error) {
|
||||
code == -32603
|
||||
message == "Something happened"
|
||||
}
|
||||
StepVerifier.create(act)
|
||||
.expectErrorMatches { t ->
|
||||
t instanceof JsonRpcException && t.error.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE
|
||||
}
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -223,27 +223,4 @@ class ResponseRpcParserSpec extends Specification {
|
||||
!act.hasResult()
|
||||
}
|
||||
|
||||
def "Keep provided ID even if JSON is not full"() {
|
||||
setup:
|
||||
def json = '{"jsonrpc": "2.0", "id": 1}'
|
||||
when:
|
||||
def act = parser.parse(json.getBytes())
|
||||
then:
|
||||
act.id.asNumber() == 1
|
||||
act.error != null
|
||||
act.hasError()
|
||||
!act.hasResult()
|
||||
}
|
||||
|
||||
def "Keep provided ID even if JSON is broken"() {
|
||||
setup:
|
||||
def json = '{"jsonrpc": "2.0", "id": 101, "resu'
|
||||
when:
|
||||
def act = parser.parse(json.getBytes())
|
||||
then:
|
||||
act.id.asNumber() == 101
|
||||
act.error != null
|
||||
act.hasError()
|
||||
!act.hasResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,24 +103,4 @@ class ResponseWSParserSpec extends Specification {
|
||||
act.error == null
|
||||
new String(act.value) == "null"
|
||||
}
|
||||
|
||||
def "Keep provided ID even if JSON is not full"() {
|
||||
setup:
|
||||
def json = '{"jsonrpc": "2.0", "id": 1}'
|
||||
when:
|
||||
def act = parser.parse(json.getBytes())
|
||||
then:
|
||||
act.id.asNumber() == 1
|
||||
act.error != null
|
||||
}
|
||||
|
||||
def "Keep provided ID even if JSON is broken"() {
|
||||
setup:
|
||||
def json = '{"jsonrpc": "2.0", "id": 101, "resu'
|
||||
when:
|
||||
def act = parser.parse(json.getBytes())
|
||||
then:
|
||||
act.id.asNumber() == 101
|
||||
act.error != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class EcdsaSignerSpec extends Specification {
|
||||
setup:
|
||||
def conf = new SignatureConfig()
|
||||
conf.enabled = true
|
||||
conf.privateKey = "src/test/resources/signer/test_key"
|
||||
conf.privateKey = "testing/dshackle/test_key"
|
||||
def signer = new ResponseSignerFactory(conf).getObject() as EcdsaSigner
|
||||
|
||||
// To verify the test, check the hash of test key above:
|
||||
@@ -114,7 +114,7 @@ class EcdsaSignerSpec extends Specification {
|
||||
|
||||
def conf = new SignatureConfig()
|
||||
conf.enabled = true
|
||||
conf.privateKey = "src/test/resources/signer/test_key"
|
||||
conf.privateKey = "testing/dshackle/test_key"
|
||||
def factory = new ResponseSignerFactory(conf)
|
||||
|
||||
def sk = factory.readKey(conf.algorithm, conf.privateKey).first
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
version: v1
|
||||
|
||||
upstreams:
|
||||
|
||||
- id: local
|
||||
chain: ethereum
|
||||
priority: 100
|
||||
connection:
|
||||
ethereum:
|
||||
rpc:
|
||||
url: "http://localhost:8545"
|
||||
|
||||
- id: infura
|
||||
chain: ethereum
|
||||
role: fallback
|
||||
priority: 50
|
||||
connection:
|
||||
ethereum:
|
||||
rpc:
|
||||
url: "https://mainnet.infura.io/v3/fa28c968191849c1aff541ad1d8511f2"
|
||||
|
||||
- id: remote
|
||||
priority: 75
|
||||
connection:
|
||||
grpc:
|
||||
host: "10.2.0.15"
|
||||
3
testing/.gitignore
vendored
Normal file
3
testing/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
gradlew
|
||||
gradlew.bat
|
||||
gradle/
|
||||
104
testing/README.adoc
Normal file
104
testing/README.adoc
Normal file
@@ -0,0 +1,104 @@
|
||||
= Dshackle Integration Testing
|
||||
|
||||
Test for a correct work using a simulation of different upstream behaviours and checking the Dshackle against it.
|
||||
|
||||
.Modules:
|
||||
- `dshackle` - Dshackle config used for testing environment
|
||||
- `simple-upstream` - upstream simulator with predefines responses
|
||||
- `trial` - actual tests
|
||||
|
||||
== How to run
|
||||
|
||||
NOTE: Run commands in project's root dir
|
||||
|
||||
NOTE: You need to open multiple consoles, since each command runs on foreground
|
||||
|
||||
== Basic Test
|
||||
|
||||
Makes some basic checks with a mocked upstreams
|
||||
|
||||
=== Run upstream emulator
|
||||
|
||||
.Runs two instances of RPC server
|
||||
[source,bash]
|
||||
----
|
||||
DSHACKLE_TESTUP_PORT=18545 ./gradlew -p testing/simple-upstream run
|
||||
DSHACKLE_TESTUP_PORT=18546 ./gradlew -p testing/simple-upstream run
|
||||
----
|
||||
|
||||
=== Run Dshackle
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew run --args="--configPath=./testing/dshackle/dshackle-basic.yaml"
|
||||
----
|
||||
|
||||
=== Run tests
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./testing/trial/gradlew -p testing/trial -PdshackleTrialMode=basic cleanTest test
|
||||
----
|
||||
|
||||
== Real (Mainnet) Test
|
||||
|
||||
=== Run Dshackle
|
||||
|
||||
First you have to provide configuration to an upstream connected to the mainnet.
|
||||
It may be a local Geth instance, or an other provider, like Infura (note that the test config doesn't support TLS or other auth)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
export DSHACKLE_TEST_ETH1_RPC=
|
||||
export DSHACKLE_TEST_ETH1_WS=
|
||||
export DSHACKLE_TEST_ETH1_WSORIGIN=
|
||||
----
|
||||
|
||||
For a local Geth it would be:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
export DSHACKLE_TEST_ETH1_RPC=http://127.0.0.1:8545
|
||||
export DSHACKLE_TEST_ETH1_WS=ws://127.0.0.1:8546
|
||||
export DSHACKLE_TEST_ETH1_WSORIGIN=http://127.0.0.1:8546
|
||||
----
|
||||
|
||||
Finally, run the Dshackle instance:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew run --args="--configPath=./testing/dshackle/dshackle-real.yaml"
|
||||
----
|
||||
|
||||
=== Run tests
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./testing/trial/gradlew -p testing/trial -PdshackleTrialMode=real cleanTest test
|
||||
----
|
||||
|
||||
== Real (Rinkeby) Test
|
||||
|
||||
Runs requests against a Rinkeby Testnet.
|
||||
Configuration is similar to the Mainnet config, but instead of `ETH1` is has `RINKEBY`.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
export DSHACKLE_TEST_RINKEBY_RPC=
|
||||
export DSHACKLE_TEST_RINKEBY_WS=
|
||||
export DSHACKLE_TEST_RINKEBY_WSORIGIN=
|
||||
----
|
||||
|
||||
And run the Dshackle instance:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew run --args="--configPath=./testing/dshackle/dshackle-rinkeby.yaml"
|
||||
----
|
||||
|
||||
=== Run tests
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./testing/trial/gradlew -p testing/trial -PdshackleTrialMode=rinkeby cleanTest test
|
||||
----
|
||||
14
testing/dshackle/README.adoc
Normal file
14
testing/dshackle/README.adoc
Normal file
@@ -0,0 +1,14 @@
|
||||
= Testing Server
|
||||
|
||||
.Run from project root directory:
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew run --args="--configPath=./testing/dshackle/dshackle-mock.yaml"
|
||||
----
|
||||
|
||||
or
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew run --args="--configPath=./testing/dshackle/dshackle-real.yaml"
|
||||
----
|
||||
50
testing/dshackle/dshackle-basic.yaml
Normal file
50
testing/dshackle/dshackle-basic.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
version: v1
|
||||
port: 12448
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
|
||||
cluster:
|
||||
upstreams:
|
||||
- id: test-1
|
||||
node-id: 1
|
||||
chain: ethereum
|
||||
methods:
|
||||
enabled:
|
||||
- name: debug_traceTransaction
|
||||
- name: test_foo
|
||||
options:
|
||||
disable-validation: true
|
||||
connection:
|
||||
ethereum-pos:
|
||||
execution:
|
||||
rpc:
|
||||
url: "http://localhost:18545"
|
||||
|
||||
- id: test-2
|
||||
node-id: 2
|
||||
chain: ethereum
|
||||
options:
|
||||
disable-validation: true
|
||||
connection:
|
||||
execution:
|
||||
ethereum-pos:
|
||||
rpc:
|
||||
url: "http://localhost:18546"
|
||||
|
||||
cache:
|
||||
redis:
|
||||
enabled: false
|
||||
|
||||
signed-response:
|
||||
enabled: true
|
||||
algorithm: SECP256K1
|
||||
private-key: "./test_key"
|
||||
|
||||
proxy:
|
||||
port: 18080
|
||||
tls:
|
||||
enabled: false
|
||||
routes:
|
||||
- id: eth
|
||||
blockchain: ethereum
|
||||
30
testing/dshackle/dshackle-real.yaml
Normal file
30
testing/dshackle/dshackle-real.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
version: v1
|
||||
port: 12448
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
|
||||
cluster:
|
||||
upstreams:
|
||||
- id: eth-1
|
||||
chain: ethereum
|
||||
connection:
|
||||
ethereum:
|
||||
rpc:
|
||||
url: "${DSHACKLE_TEST_ETH1_RPC}"
|
||||
ws:
|
||||
url: "${DSHACKLE_TEST_ETH1_WS}"
|
||||
origin: "${DSHACKLE_TEST_ETH1_WSORIGIN}"
|
||||
|
||||
cache:
|
||||
redis:
|
||||
enabled: false
|
||||
|
||||
proxy:
|
||||
port: 18081
|
||||
preserve-batch-order: true
|
||||
tls:
|
||||
enabled: false
|
||||
routes:
|
||||
- id: eth
|
||||
blockchain: ethereum
|
||||
26
testing/dshackle/dshackle-rinkeby.yaml
Normal file
26
testing/dshackle/dshackle-rinkeby.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
version: v1
|
||||
port: 12448
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
|
||||
cluster:
|
||||
upstreams:
|
||||
- id: rinkeby-1
|
||||
chain: rinkeby
|
||||
connection:
|
||||
ethereum:
|
||||
rpc:
|
||||
url: "${DSHACKLE_TEST_RINKEBY_RPC}"
|
||||
|
||||
cache:
|
||||
redis:
|
||||
enabled: false
|
||||
|
||||
proxy:
|
||||
port: 18081
|
||||
tls:
|
||||
enabled: false
|
||||
routes:
|
||||
- id: rinkeby
|
||||
blockchain: rinkeby
|
||||
6
testing/simple-upstream/README.adoc
Normal file
6
testing/simple-upstream/README.adoc
Normal file
@@ -0,0 +1,6 @@
|
||||
= Simple Testing Upstream
|
||||
|
||||
.Run from current directory
|
||||
----
|
||||
./gradlew run
|
||||
----
|
||||
22
testing/simple-upstream/build.gradle
Normal file
22
testing/simple-upstream/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'groovy'
|
||||
id 'idea'
|
||||
id 'application'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.sparkjava:spark-core:2.9.1"
|
||||
implementation "org.codehaus.groovy:groovy:3.0.4"
|
||||
implementation "com.fasterxml.jackson.core:jackson-core:2.9.8"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:2.9.8"
|
||||
}
|
||||
|
||||
application {
|
||||
mainClassName = 'testing.SimpleUpstream'
|
||||
}
|
||||
1
testing/simple-upstream/settings.gradle
Normal file
1
testing/simple-upstream/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'dshackle-testing-simple-upstream'
|
||||
@@ -0,0 +1,32 @@
|
||||
package testing
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
class BlocksHandler implements CallHandler {
|
||||
|
||||
ObjectMapper objectMapper
|
||||
ResourceResponse resourceResponse
|
||||
|
||||
BlocksHandler(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper
|
||||
this.resourceResponse = new ResourceResponse(objectMapper)
|
||||
}
|
||||
|
||||
@Override
|
||||
Result handle(String method, List<Object> params) {
|
||||
if (method == "eth_blockNumber") {
|
||||
return Result.ok("0x100001")
|
||||
}
|
||||
if (method == "eth_getBlockByNumber") {
|
||||
String blockId = params[0]
|
||||
println("get block $blockId")
|
||||
return resourceResponse.respondWith("block-${blockId}.json")
|
||||
}
|
||||
if (method == "eth_getTransactionByHash") {
|
||||
String txId = params[0]
|
||||
return resourceResponse.respondWith("tx-${txId}.json")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package testing
|
||||
|
||||
interface CallHandler {
|
||||
|
||||
Result handle(String method, List<Object> params)
|
||||
|
||||
static class Result {
|
||||
private Object result
|
||||
private Integer errorCode
|
||||
private String errorMessage
|
||||
private Object errorDetails
|
||||
|
||||
Result(Object result, Integer errorCode, String errorMessage, Object errorDetails) {
|
||||
this.result = result
|
||||
this.errorCode = errorCode
|
||||
this.errorMessage = errorMessage
|
||||
this.errorDetails = errorDetails
|
||||
}
|
||||
|
||||
static Result ok(Object result) {
|
||||
return new Result(result, null, null, null)
|
||||
}
|
||||
|
||||
static Result error(int errorCode, String errorMessage, Object details = null) {
|
||||
return new Result(null, errorCode, errorMessage, details)
|
||||
}
|
||||
|
||||
boolean isResult() {
|
||||
return errorCode == null
|
||||
}
|
||||
|
||||
Object getResult() {
|
||||
return result
|
||||
}
|
||||
|
||||
int getErrorCode() {
|
||||
return errorCode
|
||||
}
|
||||
|
||||
String getErrorMessage() {
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
Object getErrorDetails() {
|
||||
return errorDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package testing
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
class CommonHandlers implements CallHandler {
|
||||
|
||||
ObjectMapper objectMapper
|
||||
ResourceResponse resourceResponse
|
||||
|
||||
CommonHandlers(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper
|
||||
this.resourceResponse = new ResourceResponse(objectMapper)
|
||||
}
|
||||
|
||||
@Override
|
||||
Result handle(String method, List<Object> params) {
|
||||
if (method == "eth_syncing") {
|
||||
return Result.ok(false)
|
||||
}
|
||||
if (method == "eth_chainId") {
|
||||
return resourceResponse.respondWith("chain-id.json")
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package testing
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class InternalHandler implements CallHandler {
|
||||
|
||||
private AtomicInteger ids = new AtomicInteger()
|
||||
private List<Item> requests = new ArrayList()
|
||||
|
||||
def record(String json) {
|
||||
requests << new Item(ids.getAndIncrement(), json)
|
||||
}
|
||||
|
||||
@Override
|
||||
Result handle(String method, List<Object> params) {
|
||||
if (method == "eth_call"
|
||||
&& params[0].to?.toLowerCase() == "0x0123456789abcdef0123456789abcdef00000002".toLowerCase()) {
|
||||
return Result.ok(Collections.unmodifiableList(requests))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
class Item {
|
||||
Integer id
|
||||
String timestamp
|
||||
String json
|
||||
|
||||
Item(Integer id, String json) {
|
||||
this.id = id
|
||||
this.timestamp = Instant.now().toString()
|
||||
this.json = json
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package testing
|
||||
|
||||
class InvalidCallHandler implements CallHandler {
|
||||
@Override
|
||||
Result handle(String method, List<Object> params) {
|
||||
return Result.error(-32000, "Call is not supported")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package testing
|
||||
|
||||
class PingPongHandler implements CallHandler {
|
||||
@Override
|
||||
Result handle(String method, List<Object> params) {
|
||||
if (method == "eth_call"
|
||||
&& params[0].to?.toLowerCase() == "0x0123456789abcdef0123456789abcdef00000001".toLowerCase()) {
|
||||
return Result.ok([data: params[0].data])
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package testing
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
class ResourceResponse {
|
||||
|
||||
ObjectMapper objectMapper
|
||||
|
||||
ResourceResponse(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper
|
||||
}
|
||||
|
||||
Object getResource(String name) {
|
||||
String json = BlocksHandler.class.getResourceAsStream("/" + name)?.text
|
||||
if (json == null) {
|
||||
return null
|
||||
}
|
||||
return objectMapper.readValue(json, Map)
|
||||
}
|
||||
|
||||
CallHandler.Result respondWith(String name) {
|
||||
return CallHandler.Result.ok(getResource(name))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package testing
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import spark.Spark
|
||||
|
||||
class SimpleUpstream {
|
||||
|
||||
private int port = System.getenv("DSHACKLE_TESTUP_PORT")?.toInteger() ?: 18545
|
||||
private ObjectMapper objectMapper
|
||||
|
||||
private List<CallHandler> handlers = []
|
||||
private InternalHandler internalHandler
|
||||
|
||||
void prepare() {
|
||||
objectMapper = new ObjectMapper()
|
||||
|
||||
internalHandler = new InternalHandler()
|
||||
|
||||
handlers << new TestcaseHandler(objectMapper)
|
||||
handlers << new CommonHandlers(objectMapper)
|
||||
handlers << new BlocksHandler(objectMapper)
|
||||
handlers << new PingPongHandler()
|
||||
handlers << internalHandler
|
||||
|
||||
handlers << new InvalidCallHandler()
|
||||
}
|
||||
|
||||
void start() {
|
||||
println("Starting upstream on 0.0.0.0:$port")
|
||||
Spark.port(port)
|
||||
Spark.post("/") { req, resp ->
|
||||
try {
|
||||
def requestBody = req.body()
|
||||
println("request: $requestBody")
|
||||
internalHandler.record(requestBody)
|
||||
Map json = objectMapper.readValue(requestBody, Map)
|
||||
def id = json["id"]
|
||||
String method = json["method"].toString()
|
||||
List<Object> params = json.containsKey("params") ? json["params"] as List<Object> : []
|
||||
|
||||
CallHandler.Result result = handlers.findResult { h ->
|
||||
return h.handle(method, params)
|
||||
}
|
||||
Map resultJson = [
|
||||
id : id,
|
||||
jsonrpc: "2.0"
|
||||
]
|
||||
if (result.isResult()) {
|
||||
resultJson["result"] = result.result
|
||||
} else {
|
||||
resultJson["error"] = [
|
||||
code : result.getErrorCode(),
|
||||
message: result.getErrorMessage()
|
||||
]
|
||||
if (result.getErrorDetails() != null) {
|
||||
resultJson["error"]["data"] = result.getErrorDetails()
|
||||
}
|
||||
}
|
||||
resp.status(200)
|
||||
resp.header("content-type", "application/json")
|
||||
return objectMapper.writeValueAsString(resultJson)
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
def server = new SimpleUpstream()
|
||||
server.prepare()
|
||||
server.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package testing
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
class TestcaseHandler implements CallHandler {
|
||||
|
||||
ObjectMapper objectMapper
|
||||
ResourceResponse resourceResponse
|
||||
|
||||
TestcaseHandler(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper
|
||||
this.resourceResponse = new ResourceResponse(objectMapper)
|
||||
}
|
||||
|
||||
@Override
|
||||
Result handle(String method, List<Object> params) {
|
||||
// https://github.com/emeraldpay/dshackle/issues/35
|
||||
if (method == "eth_call"
|
||||
&& params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) {
|
||||
return Result.error(-32015, "VM execution error.")
|
||||
}
|
||||
// https://github.com/emeraldpay/dshackle/issues/35 (second)
|
||||
if (method == "eth_call"
|
||||
&& params[0].to?.toLowerCase() == "0x8ee2a5aca4f88cb8c757b8593d0734855dcc0eba".toLowerCase()) {
|
||||
return Result.error(-32015, "VM execution error.", "revert: SafeMath: division by zero")
|
||||
}
|
||||
// https://github.com/emeraldpay/dshackle/issues/43
|
||||
if (method == "debug_traceTransaction"
|
||||
&& params[0].toLowerCase() == "0xd949bc0fe1a5d16f4522bc47933554dcc4ada0493ff71ee1973b2410257af9fe".toLowerCase()) {
|
||||
return resourceResponse.respondWith("trace-0xd949bc.json")
|
||||
}
|
||||
// https://github.com/emeraldpay/dshackle/issues/67
|
||||
if (method == "eth_call"
|
||||
&& params[0].to?.toLowerCase() == "0xdAC17F958D2ee523a2206206994597C13D831ec7".toLowerCase()) {
|
||||
return Result.error(-32000, "invalid opcode: opcode 0xfe not defined")
|
||||
}
|
||||
// https://github.com/emeraldpay/dshackle/issues/67, when a custom method configured
|
||||
if (method == "test_foo"
|
||||
&& params[0].to?.toLowerCase() == "0xdAC17F958D2ee523a2206206994597C13D831ec7".toLowerCase()) {
|
||||
return Result.error(-32000, "invalid opcode: opcode 0xfe not defined")
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"difficulty": "0xc6d2fa46fd6",
|
||||
"extraData": "0xd783010400844765746887676f312e352e31856c696e7578",
|
||||
"gasLimit": "0x2fefd8",
|
||||
"gasUsed": "0xa410",
|
||||
"hash": "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e",
|
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"miner": "0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da",
|
||||
"mixHash": "0x72224fd0b7b32d0fea13f2953eba28d2512af43632725ed75583dd30e42dfc64",
|
||||
"nonce": "0xf1a453a44f9de627",
|
||||
"number": "0x100000",
|
||||
"parentHash": "0x2dcaf9a8a3fd329d925eb47c1f22022d4f2d562500e546d878bad4247d013858",
|
||||
"receiptsRoot": "0x178000485de197ed06a4ebada431864e5b564b43f994fb8888521f74b4b3257b",
|
||||
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
|
||||
"size": "0x303",
|
||||
"stateRoot": "0xeaddaa0b6b673ba3f8cb7235ca656340ccf703295cf5252cb04a4202c47252c2",
|
||||
"timestamp": "0x56cc7b73",
|
||||
"totalDifficulty": "0x6bab93cdf810b22a",
|
||||
"transactions": [
|
||||
"0x01c5a8461d06c2c195035c148af0f871c7679841d86ae5bb98676bb2d8e68dfa",
|
||||
"0xb4216e88df6ebfe666bbe43370d1ddfe8d9c1975d2b8375a522700f7f59c3ed0"
|
||||
],
|
||||
"transactionsRoot": "0x91d9b419b34a790cc2a7d59c6b95e57ccc5ec9bc1034ce9510dfb551d3049c93",
|
||||
"uncles": []
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"difficulty": "0xc6ba1fe7c49",
|
||||
"extraData": "0xd783010400844765746887676f312e352e31856c696e7578",
|
||||
"gasLimit": "0x2fefd8",
|
||||
"gasUsed": "0xa410",
|
||||
"hash": "0x18c68d9ba58772a4409d65d61891b25db03a105a7769ae08ef2cff697921b446",
|
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"miner": "0x0c729be7c39543c3d549282a40395299d987cec2",
|
||||
"mixHash": "0xf8679fe0f04d5aad4844694cc8fb1a1b97482a5822658379c9bb8633f91daf97",
|
||||
"nonce": "0x347a267c0ec88618",
|
||||
"number": "0x100001",
|
||||
"parentHash": "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e",
|
||||
"receiptsRoot": "0x4f4ed1139baadbd4506d6b0c330335d2108715095fb93fd282bd69aa9edc09eb",
|
||||
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
|
||||
"size": "0x304",
|
||||
"stateRoot": "0xee13382462fcd5748a403ca131c38e871fa7206bca2b962f67b59e800741daa3",
|
||||
"timestamp": "0x56cc7b8c",
|
||||
"totalDifficulty": "0x6baba0399a0f2e73",
|
||||
"transactions": [
|
||||
"0x146b8f4b6300c73bb7476359b9f1c5ee3f686a86b2aa673552cf0f9de9a42e77",
|
||||
"0xe589a39acea3091b584b650158d08b159aa07e97b8e8cddb8f81cb606e13382e"
|
||||
],
|
||||
"transactionsRoot": "0xc90078e2af52aef81815cb2a71c22ebd781dd658dd953d9df57f7769a0b2fe51",
|
||||
"uncles": [],
|
||||
"testFoo": "bar"
|
||||
}
|
||||
5
testing/simple-upstream/src/main/resources/chain-id.json
Normal file
5
testing/simple-upstream/src/main/resources/chain-id.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": 83,
|
||||
"jsonrpc": "2.0",
|
||||
"result": "0x3d"
|
||||
}
|
||||
6966
testing/simple-upstream/src/main/resources/trace-0xd949bc.json
Normal file
6966
testing/simple-upstream/src/main/resources/trace-0xd949bc.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"blockHash": "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e",
|
||||
"blockNumber": "0x100000",
|
||||
"from": "0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da",
|
||||
"gas": "0x15f90",
|
||||
"gasPrice": "0xba43b7400",
|
||||
"hash": "0x01c5a8461d06c2c195035c148af0f871c7679841d86ae5bb98676bb2d8e68dfa",
|
||||
"input": "0x",
|
||||
"nonce": "0x2f31",
|
||||
"r": "0x1d263f32de00456cbc3e4099c623a6ca48e3188f01dd16461687512f8c3bb7d6",
|
||||
"s": "0x62629f42a98f7c5fa60f8c3630cf67b5eb4772e0ff65a458d2116c15e22d6451",
|
||||
"to": "0xdae787ec66e65c60ad35203800b97b45fcb0f909",
|
||||
"transactionIndex": "0x0",
|
||||
"v": "0x1b",
|
||||
"value": "0x2b81097c919a9e000"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"blockHash": "0x18c68d9ba58772a4409d65d61891b25db03a105a7769ae08ef2cff697921b446",
|
||||
"blockNumber": "0x100001",
|
||||
"from": "0x2a65aca4d5fc5b5c859090a6c34d164135398226",
|
||||
"gas": "0x15f90",
|
||||
"gasPrice": "0xba43b7400",
|
||||
"hash": "0x146b8f4b6300c73bb7476359b9f1c5ee3f686a86b2aa673552cf0f9de9a42e77",
|
||||
"input": "0x",
|
||||
"nonce": "0x32c13",
|
||||
"r": "0xe6ca408c2e91163b46c6bb66054a000a25b6c1d8024460499a185ab2e4bb41a0",
|
||||
"s": "0x53bc43662fda46c2e7051fa704b75ec815e9102cca5400817011ffd99e77139d",
|
||||
"to": "0x7388ae5bc873163025fffe5956322f734f9b570d",
|
||||
"transactionIndex": "0x0",
|
||||
"v": "0x1b",
|
||||
"value": "0xe660f79cf5e4800"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"blockHash": "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e",
|
||||
"blockNumber": "0x100000",
|
||||
"from": "0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da",
|
||||
"gas": "0x15f90",
|
||||
"gasPrice": "0xba43b7400",
|
||||
"hash": "0xb4216e88df6ebfe666bbe43370d1ddfe8d9c1975d2b8375a522700f7f59c3ed0",
|
||||
"input": "0x",
|
||||
"nonce": "0x2f32",
|
||||
"r": "0xc25ceef654cd3bd734201fc08589970e0663431121bc47cc4a70335e072b09c0",
|
||||
"s": "0x1c67b037a59a8f5badde3a7eb586d7888dcf6dad64aabb080c4c0db593a91254",
|
||||
"to": "0x04a2a3ee9c9ea82e1f918ece209f8a7e6d7521e7",
|
||||
"transactionIndex": "0x1",
|
||||
"v": "0x1c",
|
||||
"value": "0x377fb2f2cbd73c00"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"blockHash": "0x18c68d9ba58772a4409d65d61891b25db03a105a7769ae08ef2cff697921b446",
|
||||
"blockNumber": "0x100001",
|
||||
"from": "0x2a65aca4d5fc5b5c859090a6c34d164135398226",
|
||||
"gas": "0x15f90",
|
||||
"gasPrice": "0xba43b7400",
|
||||
"hash": "0xe589a39acea3091b584b650158d08b159aa07e97b8e8cddb8f81cb606e13382e",
|
||||
"input": "0x",
|
||||
"nonce": "0x32c14",
|
||||
"r": "0x77fe7cfb03258f2eed8ffbbdc19f73f4a6095a55a7e06f81ade02f3435f7e249",
|
||||
"s": "0x821ad733ceecfa7567e9c8ffb66e3f8bbc3b2b7b21bacadb2a065e57b2a6992",
|
||||
"to": "0x1c774e876a46f51e305add171ba3bb4589408273",
|
||||
"transactionIndex": "0x1",
|
||||
"v": "0x1b",
|
||||
"value": "0xe5ef66c73b5fc00"
|
||||
}
|
||||
8
testing/trial/README.adoc
Normal file
8
testing/trial/README.adoc
Normal file
@@ -0,0 +1,8 @@
|
||||
= Actual Tests
|
||||
|
||||
Expects running _Simple Upstream_ and _Testing Dshackle_.
|
||||
|
||||
.Run from current directory
|
||||
----
|
||||
./gradlew check
|
||||
----
|
||||
35
testing/trial/build.gradle
Normal file
35
testing/trial/build.gradle
Normal file
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'groovy'
|
||||
id 'idea'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url "https://maven.emrld.io" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.apache.httpcomponents:httpclient:4.5.12"
|
||||
implementation "org.codehaus.groovy:groovy:3.0.4"
|
||||
implementation "com.fasterxml.jackson.core:jackson-core:2.9.8"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:2.9.8"
|
||||
implementation "io.grpc:grpc-netty:1.46.0"
|
||||
implementation "org.bouncycastle:bcprov-jdk15on:1.61"
|
||||
implementation("io.emeraldpay:emerald-api:0.12-alpha.1") {
|
||||
exclude group: 'com.salesforce.servicelibs', module: 'reactor-grpc'
|
||||
}
|
||||
|
||||
testImplementation "org.spockframework:spock-core:2.0-M3-groovy-3.0"
|
||||
}
|
||||
|
||||
|
||||
test {
|
||||
systemProperty "trialMode", project.getProperty("dshackleTrialMode")
|
||||
systemProperty "signatureKey", project.getProperty("signatureKey")
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events "PASSED", "FAILED"
|
||||
}
|
||||
}
|
||||
1
testing/trial/settings.gradle
Normal file
1
testing/trial/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'dshackle-testing-trial'
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.emeraldpay.dshackle.testing.trial
|
||||
|
||||
interface Client {
|
||||
Map<String, Object> execute(String method, List<Object> params)
|
||||
Map<String, Object> execute(Object id, String method, List<Object> params)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.emeraldpay.dshackle.testing.trial
|
||||
|
||||
class Debugger {
|
||||
|
||||
public static enabled = true
|
||||
|
||||
static void showOut(String json) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
println(">> $json")
|
||||
}
|
||||
|
||||
static void showIn(String json) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
println("<< $json")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package io.emeraldpay.dshackle.testing.trial
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.google.common.primitives.Bytes
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.grpc.ManagedChannel
|
||||
import io.grpc.netty.NettyChannelBuilder
|
||||
|
||||
class ProtoClient implements Client {
|
||||
private int sequence = 0;
|
||||
private ObjectMapper objectMapper;
|
||||
private ReactorBlockchainGrpc.ReactorBlockchainStub stub
|
||||
private Chain chain
|
||||
|
||||
ProtoClient(ManagedChannel channel, Chain chain) {
|
||||
this.stub = ReactorBlockchainGrpc.newReactorStub(channel)
|
||||
this.objectMapper = new ObjectMapper()
|
||||
this.chain= chain
|
||||
}
|
||||
|
||||
static ProtoClient create(String host, int port, Chain chain) {
|
||||
def channel = NettyChannelBuilder.forAddress(host, port)
|
||||
.maxInboundMessageSize(Integer.MAX_VALUE)
|
||||
.usePlaintext()
|
||||
new ProtoClient(channel.build(), chain)
|
||||
}
|
||||
|
||||
static ProtoClient basic() {
|
||||
create("localhost", 12448, Chain.ETHEREUM)
|
||||
}
|
||||
|
||||
BlockchainOuterClass.NativeCallReplyItem executeNative(String method, List<Object> params, Long nonce) {
|
||||
def req = BlockchainOuterClass.NativeCallRequest
|
||||
.newBuilder()
|
||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||
.addItems(BlockchainOuterClass.NativeCallItem
|
||||
.newBuilder()
|
||||
.setId(0)
|
||||
.setNonce(nonce)
|
||||
.setMethod(method)
|
||||
.setPayload(ByteString.copyFrom(objectMapper.writeValueAsBytes(params)))
|
||||
.build()
|
||||
).build()
|
||||
stub.nativeCall(req)
|
||||
.single()
|
||||
.block()
|
||||
}
|
||||
|
||||
Map<String, Object> execute(String method, List<Object> params) {
|
||||
return execute(sequence++, method, params)
|
||||
}
|
||||
Map<String, Object> execute(Object id, String method, List<Object> params) {
|
||||
def result = executeNative(method, params, 0L)
|
||||
if (result.errorMessage != "") {
|
||||
return [error: result.errorMessage]
|
||||
} else {
|
||||
return objectMapper.readerFor(Map).readValue(Bytes.concat("{\"result\": ".bytes, result.payload.toByteArray(), "}".bytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.emeraldpay.dshackle.testing.trial
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import org.apache.http.HttpHeaders
|
||||
import org.apache.http.client.HttpClient
|
||||
import org.apache.http.client.methods.CloseableHttpResponse
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
import org.apache.http.entity.ContentType
|
||||
import org.apache.http.entity.StringEntity
|
||||
import org.apache.http.impl.client.CloseableHttpClient
|
||||
import org.apache.http.impl.client.HttpClientBuilder
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
|
||||
class ProxyClient implements Client {
|
||||
|
||||
private int sequence = 0;
|
||||
|
||||
private String url
|
||||
private ObjectMapper objectMapper
|
||||
|
||||
ProxyClient(String url) {
|
||||
this.url = url
|
||||
this.objectMapper = new ObjectMapper()
|
||||
}
|
||||
|
||||
static ProxyClient ethereumMock() {
|
||||
return forPrefix(18080, "eth")
|
||||
}
|
||||
|
||||
static ProxyClient ethereumReal() {
|
||||
return forPrefix(18081, "eth")
|
||||
}
|
||||
|
||||
static ProxyClient ethereumRinkeby() {
|
||||
return forPrefix(18081, "rinkeby")
|
||||
}
|
||||
|
||||
static ProxyClient forPrefix(String prefix) {
|
||||
return forPrefix(18080, prefix)
|
||||
}
|
||||
|
||||
static ProxyClient forPrefix(int port, String prefix) {
|
||||
return new ProxyClient("http://127.0.0.1:$port/$prefix")
|
||||
}
|
||||
|
||||
static ProxyClient forOriginal(int port) {
|
||||
return new ProxyClient("http://127.0.0.1:$port")
|
||||
}
|
||||
|
||||
Map<String, Object> execute(String method, List<Object> params) {
|
||||
return execute(sequence++, method, params)
|
||||
}
|
||||
|
||||
Map<String, Object> execute(Object id, String method, List<Object> params) {
|
||||
Map jsonData = [
|
||||
id : id,
|
||||
jsonrpc: "2.0",
|
||||
method : method,
|
||||
params : params
|
||||
]
|
||||
String reqJson = objectMapper.writeValueAsString(jsonData)
|
||||
Debugger.showOut(reqJson)
|
||||
CloseableHttpClient httpClient = null
|
||||
try {
|
||||
httpClient = HttpClients.createDefault()
|
||||
def request = new HttpPost(url)
|
||||
request.setEntity(new StringEntity(reqJson, ContentType.APPLICATION_JSON))
|
||||
CloseableHttpResponse response = httpClient.execute(request)
|
||||
if (response.getStatusLine().statusCode != 200) {
|
||||
throw new IllegalStateException("Non-ok status: " + response.getStatusLine().statusCode)
|
||||
}
|
||||
String respJson = response.entity.content.text
|
||||
Debugger.showIn(respJson)
|
||||
return objectMapper.readerFor(Map).readValue(respJson)
|
||||
} finally {
|
||||
httpClient?.close()
|
||||
}
|
||||
|
||||
return [error: "NOT EXECUTED"]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.basicproxy
|
||||
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'basic' })
|
||||
class DispatchSpec extends Specification {
|
||||
|
||||
def client = ProxyClient.forPrefix("eth")
|
||||
|
||||
def "multiple calls routed roughly equal to upstreams"() {
|
||||
when:
|
||||
def calls1before = ProxyClient.forOriginal(18545).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
def calls2before = ProxyClient.forOriginal(18546).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
|
||||
100.times {
|
||||
client.execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000001", data: "0x00000000" + Integer.toString(it, 16)]])
|
||||
}
|
||||
|
||||
def calls1after = ProxyClient.forOriginal(18545).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
def calls2after = ProxyClient.forOriginal(18546).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
|
||||
def calls1 = onlyNew(calls1before, calls1after)
|
||||
def calls2 = onlyNew(calls2before, calls2after)
|
||||
|
||||
then:
|
||||
calls1.size() >= 48
|
||||
calls2.size() >= 48
|
||||
calls1.size() + calls2.size() == 100
|
||||
}
|
||||
|
||||
def "multiple calls routed roughly equal to upstreams - for latest block"() {
|
||||
when:
|
||||
def calls1before = ProxyClient.forOriginal(18545).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
def calls2before = ProxyClient.forOriginal(18546).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
|
||||
100.times {
|
||||
client.execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000001", data: "0x00000000" + Integer.toString(it, 16)], "0x100001"])
|
||||
}
|
||||
|
||||
def calls1after = ProxyClient.forOriginal(18545).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
def calls2after = ProxyClient.forOriginal(18546).execute("eth_call", [[to: "0x0123456789abcdef0123456789abcdef00000002"]]).result as List
|
||||
|
||||
def calls1 = onlyNew(calls1before, calls1after)
|
||||
def calls2 = onlyNew(calls2before, calls2after)
|
||||
|
||||
then:
|
||||
calls1.size() >= 48
|
||||
calls2.size() >= 48
|
||||
calls1.size() + calls2.size() == 100
|
||||
}
|
||||
|
||||
private List onlyNew(List before, List after) {
|
||||
return after.findAll { a ->
|
||||
!before.any { b ->
|
||||
b.id == a.id
|
||||
}
|
||||
}.findAll {
|
||||
(it.json as String).contains("0x0123456789abcdef0123456789abcdef00000001")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.basicproxy
|
||||
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'basic' })
|
||||
class GivesErrorSpec extends Specification {
|
||||
|
||||
def client = ProxyClient.forPrefix("eth")
|
||||
|
||||
def "Gives error message"() {
|
||||
// issue #42
|
||||
when:
|
||||
def act = client.execute("eth_call", [[to: "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c"]])
|
||||
then:
|
||||
act.result == null
|
||||
act.error != null
|
||||
with(act.error) {
|
||||
code == -32015
|
||||
message == "VM execution error."
|
||||
}
|
||||
}
|
||||
|
||||
def "Gives error data"() {
|
||||
// issue #42
|
||||
when:
|
||||
def act = client.execute("eth_call", [[to: "0x8ee2a5aca4f88cb8c757b8593d0734855dcc0eba"]])
|
||||
then:
|
||||
act.result == null
|
||||
act.error != null
|
||||
with(act.error) {
|
||||
code == -32015
|
||||
message == "VM execution error."
|
||||
data == "revert: SafeMath: division by zero"
|
||||
}
|
||||
}
|
||||
|
||||
def "Dispatch error from upstream when block is specified"() {
|
||||
// issue #67
|
||||
when:
|
||||
def call = [
|
||||
to : "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
from: "0xEF65ffB384c99a00403EAa22115323a555700D79",
|
||||
data: "0xa9059cbb0000000000000000000000003f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be000000000000000000000000000000000000000000000000000000004856fb60"
|
||||
]
|
||||
def act = client.execute("eth_call", [call, "0x100000"])
|
||||
then:
|
||||
act.result == null
|
||||
act.error != null
|
||||
with(act.error) {
|
||||
code == -32000
|
||||
message == "invalid opcode: opcode 0xfe not defined"
|
||||
}
|
||||
}
|
||||
|
||||
def "Dispatch error from upstream when custome method is used"() {
|
||||
// issue #67
|
||||
when:
|
||||
def call = [
|
||||
to : "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
from: "0xEF65ffB384c99a00403EAa22115323a555700D79",
|
||||
data: "0xa9059cbb0000000000000000000000003f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be000000000000000000000000000000000000000000000000000000004856fb60"
|
||||
]
|
||||
def act = client.execute("test_foo", [call, "0x100000"])
|
||||
then:
|
||||
act.result == null
|
||||
act.error != null
|
||||
with(act.error) {
|
||||
code == -32000
|
||||
message == "invalid opcode: opcode 0xfe not defined"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.basicproxy
|
||||
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'basic' })
|
||||
class HardcodedCallsSpec extends Specification {
|
||||
|
||||
def client = ProxyClient.forPrefix("eth")
|
||||
|
||||
def "get syncing"() {
|
||||
when:
|
||||
def act = client.execute("eth_syncing", [])
|
||||
then:
|
||||
act.result == false
|
||||
act.error == null
|
||||
}
|
||||
|
||||
def "get network version"() {
|
||||
when:
|
||||
def act = client.execute("net_version", [])
|
||||
then:
|
||||
act.result == "1"
|
||||
act.error == null
|
||||
}
|
||||
|
||||
def "get peer count"() {
|
||||
when:
|
||||
def act = client.execute("net_peerCount", [])
|
||||
then:
|
||||
act.result == "0x2a"
|
||||
act.error == null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.basicproxy
|
||||
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'basic' })
|
||||
class MetamaskCallSpec extends Specification {
|
||||
|
||||
def client = ProxyClient.forPrefix("eth")
|
||||
|
||||
def "use long id"() {
|
||||
when:
|
||||
def act = client.execute(1057264140543346, "net_version", [])
|
||||
then:
|
||||
act.id == 1057264140543346
|
||||
act.result != null
|
||||
act.error == null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.basicproxy
|
||||
|
||||
import com.google.common.primitives.Bytes
|
||||
import com.google.common.primitives.Longs
|
||||
import io.emeraldpay.dshackle.testing.trial.ProtoClient
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Specification
|
||||
import java.security.KeyFactory
|
||||
import org.bouncycastle.util.io.pem.PemReader
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'basic' })
|
||||
class StandardCallsSpec extends Specification {
|
||||
|
||||
@Shared client_proto = ProtoClient.basic()
|
||||
@Shared client_proxy = ProxyClient.forPrefix("eth")
|
||||
|
||||
@Shared clients = [client_proto, client_proxy]
|
||||
|
||||
def "get height"() {
|
||||
when:
|
||||
def act = client.execute("eth_blockNumber", [])
|
||||
then:
|
||||
act.result == "0x100001"
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "get block"() {
|
||||
when:
|
||||
def act = client.execute("eth_getBlockByNumber", ["0x100001", false])
|
||||
then:
|
||||
act.result != null
|
||||
with(act.result) {
|
||||
number == "0x100001"
|
||||
hash == "0x18c68d9ba58772a4409d65d61891b25db03a105a7769ae08ef2cff697921b446"
|
||||
transactions == [
|
||||
"0x146b8f4b6300c73bb7476359b9f1c5ee3f686a86b2aa673552cf0f9de9a42e77",
|
||||
"0xe589a39acea3091b584b650158d08b159aa07e97b8e8cddb8f81cb606e13382e"
|
||||
]
|
||||
}
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "get non-existing block"() {
|
||||
when:
|
||||
def act = client.execute("eth_getBlockByNumber", ["0x200001", false])
|
||||
then:
|
||||
act.result == null
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "get tx"() {
|
||||
when:
|
||||
def act = client.execute("eth_getTransactionByHash", ["0x01c5a8461d06c2c195035c148af0f871c7679841d86ae5bb98676bb2d8e68dfa"])
|
||||
then:
|
||||
act.result != null
|
||||
with(act.result) {
|
||||
blockHash == "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e"
|
||||
}
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "get non-existing tx"() {
|
||||
when:
|
||||
def act = client.execute("eth_getTransactionByHash", ["0x000000461d06c2c195035c148af0f871c7679841d86ae5bb98676bb2d8e68dfa"])
|
||||
then:
|
||||
act.result == null
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "get block with txes"() {
|
||||
when:
|
||||
def act = client.execute("eth_getBlockByNumber", ["0x100001", true])
|
||||
then:
|
||||
act.result != null
|
||||
with(act.result) {
|
||||
number == "0x100001"
|
||||
hash == "0x18c68d9ba58772a4409d65d61891b25db03a105a7769ae08ef2cff697921b446"
|
||||
transactions.size() == 2
|
||||
transactions[0] instanceof Map
|
||||
transactions[1] instanceof Map
|
||||
with(transactions.find { it.hash == "0x146b8f4b6300c73bb7476359b9f1c5ee3f686a86b2aa673552cf0f9de9a42e77" }) {
|
||||
from == "0x2a65aca4d5fc5b5c859090a6c34d164135398226"
|
||||
}
|
||||
}
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "returns original block json"() {
|
||||
when:
|
||||
def act = client.execute("eth_getBlockByNumber", ["0x100001", false])
|
||||
then:
|
||||
act.result != null
|
||||
with(act.result) {
|
||||
testFoo == "bar"
|
||||
}
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "returns original block json with tx"() {
|
||||
when:
|
||||
def act = client.execute("eth_getBlockByNumber", ["0x100001", true])
|
||||
then:
|
||||
act.result != null
|
||||
with(act.result) {
|
||||
testFoo == "bar"
|
||||
}
|
||||
act.error == null
|
||||
where:
|
||||
client << clients
|
||||
}
|
||||
|
||||
def "check response signature with nonce"() {
|
||||
when:
|
||||
def act = client_proto.executeNative("eth_blockNumber", [], 10)
|
||||
def keyFactory = KeyFactory.getInstance("EC")
|
||||
def key = new File(System.getProperty('signatureKey'))
|
||||
def reader = new PemReader(key.newReader())
|
||||
def keySpec = new X509EncodedKeySpec(reader.readPemObject().getContent())
|
||||
def pubKey = keyFactory.generatePublic(keySpec)
|
||||
def sig = Signature.getInstance("SHA256withECDSA")
|
||||
sig.initVerify(pubKey)
|
||||
def sep = "/".bytes
|
||||
def digest = MessageDigest.getInstance("SHA-256")
|
||||
def messageHash = digest.digest(act.payload.toByteArray())
|
||||
def sigmessage = Bytes.concat("DSHACKLESIG".bytes,
|
||||
sep,
|
||||
10L.toString().bytes,
|
||||
sep,
|
||||
act.signature.upstreamId.bytes,
|
||||
sep,
|
||||
Hex.encodeHexString(messageHash).bytes)
|
||||
sig.update(sigmessage)
|
||||
then:
|
||||
(new String(act.payload.toByteArray())) == "\"0x100001\""
|
||||
sig.verify(act.signature.signature.toByteArray())
|
||||
}
|
||||
|
||||
def "check response signature without nonce"() {
|
||||
when:
|
||||
def act = client_proto.executeNative("eth_blockNumber", [], 0L)
|
||||
then:
|
||||
act.signature.signature.isEmpty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.mainnet
|
||||
|
||||
import io.emeraldpay.dshackle.testing.trial.Debugger
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Timeout
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'real' })
|
||||
class GetBlockSpec extends Specification {
|
||||
|
||||
def client = ProxyClient.ethereumReal()
|
||||
|
||||
def setup() {
|
||||
Debugger.enabled = false
|
||||
}
|
||||
|
||||
def cleanup() {
|
||||
Debugger.enabled = true
|
||||
}
|
||||
|
||||
@Timeout(15)
|
||||
def "Correct order for transactions on #height"() {
|
||||
expect:
|
||||
def results = [
|
||||
(client.execute("eth_getBlockByNumber", ["0x" + Integer.toString(height, 16), false]).result["transactions"] as List<String>),
|
||||
(client.execute("eth_getBlockByNumber", ["0x" + Integer.toString(height, 16), true]).result["transactions"] as List<Map<String, Object>>)
|
||||
.collect { it.hash }
|
||||
]
|
||||
|
||||
println(results[0])
|
||||
println(results[1])
|
||||
|
||||
results[0] == results[1]
|
||||
|
||||
where:
|
||||
height << (10_000_000..10_000_500)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.emeraldpay.dshackle.testing.trial.rinkeby
|
||||
|
||||
import io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
|
||||
@IgnoreIf({ System.getProperty('trialMode') != 'rinkeby' })
|
||||
class CallContractSpec extends Specification {
|
||||
|
||||
def client = ProxyClient.ethereumRinkeby()
|
||||
|
||||
def "get block"() {
|
||||
when:
|
||||
def act = client.execute(65, "eth_call",
|
||||
[
|
||||
[
|
||||
"to" : "0xB8Ba177465b3d696180742c6ba58C408175CB6Dd",
|
||||
"data": "0x70a08231000000000000000000000000da3f87aa38d07f1fc836873a3b34ec23088ef78a"
|
||||
],
|
||||
"0xc90f1c8c125a4d5b90742f16947bdb1d10516f173fd7fc51223d10499de2a812"
|
||||
]
|
||||
)
|
||||
then:
|
||||
act.id == 65
|
||||
act.result != null
|
||||
act.result == "0x000000000000000000000000000000000000000000000000000000003af9a800"
|
||||
act.error == null
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user