solution: Websocket Ethereum proxy

fix: #110
This commit is contained in:
Igor Artamonov
2021-10-26 23:03:40 -04:00
parent 6158d10aac
commit bdd25da387
12 changed files with 681 additions and 204 deletions

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.proxy
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.grpc.Chain
import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Duration
class BaseHandlerSpec extends Specification {
def requestHandler = new AccessHandlerHttp.NoOpHandler()
def "Return empty for empty single call"() {
setup:
def handler = new BaseHandlerImpl(new WriteRpcJson(), Stub(NativeCall), Stub(ProxyServer.RequestMetricsFactory))
when:
def act = Mono.from(handler.execute(Chain.ETHEREUM, new ProxyCall(ProxyCall.RpcType.SINGLE), requestHandler))
.block(Duration.ofSeconds(1))
then:
act == ""
}
def "Return empty array for empty batch call"() {
setup:
def handler = new BaseHandlerImpl(new WriteRpcJson(), Stub(NativeCall), Stub(ProxyServer.RequestMetricsFactory))
when:
def act = Mono.from(handler.execute(Chain.ETHEREUM, new ProxyCall(ProxyCall.RpcType.BATCH), requestHandler))
.block(Duration.ofSeconds(1))
then:
act == "[]"
}
def "Execute single call"() {
setup:
def nativeCall = Mock(NativeCall)
def handler = new BaseHandlerImpl(new WriteRpcJson(), nativeCall, Stub(ProxyServer.RequestMetricsFactory))
def request = BlockchainOuterClass.NativeCallItem.newBuilder()
.setMethod("eth_test")
.setId(0)
.build()
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.items.add(request)
call.ids[0] = 5
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler))
.collectList()
.block(Duration.ofSeconds(1))
.join("")
then:
act == '{"jsonrpc":"2.0","id":5,"result":{"foo": 1}}'
1 * nativeCall.nativeCallResult(_) >> Flux.fromIterable([response])
}
def "Execute batch call with one item"() {
setup:
def nativeCall = Mock(NativeCall)
def handler = new BaseHandlerImpl(new WriteRpcJson(), nativeCall, Stub(ProxyServer.RequestMetricsFactory))
def request = BlockchainOuterClass.NativeCallItem.newBuilder()
.setMethod("eth_test")
.setId(0)
.build()
def call = new ProxyCall(ProxyCall.RpcType.BATCH)
call.items.add(request)
call.ids[0] = 5
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler))
.collectList()
.block(Duration.ofSeconds(1))
.join("")
then:
act == '[{"jsonrpc":"2.0","id":5,"result":{"foo": 1}}]'
1 * nativeCall.nativeCallResult(_) >> Flux.fromIterable([response])
}
class BaseHandlerImpl extends BaseHandler {
BaseHandlerImpl(@NotNull WriteRpcJson writeRpcJson, @NotNull NativeCall nativeCall, @NotNull ProxyServer.RequestMetricsFactory requestMetrics) {
super(writeRpcJson, nativeCall, requestMetrics)
}
}
}

View File

@@ -1,6 +1,5 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
* Copyright (c) 2020 EmeraldPay, Inc
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,11 +18,8 @@ package io.emeraldpay.dshackle.proxy
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.TlsSetup
import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.grpc.Chain
@@ -35,64 +31,7 @@ import spock.lang.Specification
import java.time.Duration
import java.util.function.Function
class ProxyServerSpec extends Specification {
def "Uses NativeCall"() {
setup:
NativeCall nativeCall = Mock(NativeCall)
def predefined = { a -> Flux.just("hello") } as Function
WriteRpcJson writeRpcJson = Mock {
1 * toJsons(_) >> predefined
}
ProxyServer server = new ProxyServer(
new ProxyConfig(),
new ReadRpcJson(),
writeRpcJson,
nativeCall,
new TlsSetup(TestingCommons.fileResolver()),
new AccessHandlerHttp.NoOpFactory()
)
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
call.items.add(
BlockchainOuterClass.NativeCallItem.newBuilder()
.setMethod("eth_hello")
.build()
)
when:
def act = server.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler())
then:
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
StepVerifier.create(act)
.expectNext("hello")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Return error on invalid request"() {
setup:
ReadRpcJson read = Mock(ReadRpcJson) {
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.NumberId(4)) }
}
def server = new ProxyServer(
Stub(ProxyConfig),
read,
Stub(WriteRpcJson), Stub(NativeCall), Stub(TlsSetup),
new AccessHandlerHttp.NoOpFactory()
)
when:
def act = server.processRequest(Chain.ETHEREUM, Mono.just("".bytes), new AccessHandlerHttp.NoOpHandler())
.map { new String(it.array()) }
then:
StepVerifier.create(act)
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
.expectComplete()
.verify(Duration.ofSeconds(1))
}
class HttpHandlerSpec extends Specification {
def "Calls access log handler"() {
setup:
@@ -107,30 +46,78 @@ class ProxyServerSpec extends Specification {
.addItems(reqItem)
.build()
ReadRpcJson read = Mock(ReadRpcJson) {
1 * apply(_) >> new ProxyCall(ProxyCall.RpcType.SINGLE).tap { it.items.add(reqItem) }
}
NativeCall nativeCall = Mock(NativeCall) {
1 * nativeCallResult(_) >> Flux.fromIterable([respItem])
}
def handler = Mock(AccessHandlerHttp.RequestHandler.class)
def server = new ProxyServer(
Stub(ProxyConfig),
read,
new WriteRpcJson(),
nativeCall,
Stub(TlsSetup),
new AccessHandlerHttp.NoOpFactory()
def accessHandler = Mock(AccessHandlerHttp.RequestHandler)
def accessHandlerFactory = Mock(AccessHandlerHttp.HandlerFactory) {
_ * it.create(_,) >> accessHandler
}
def handler = new HttpHandler(
new ReadRpcJson(), new WriteRpcJson(),
nativeCall, accessHandlerFactory, Stub(ProxyServer.RequestMetricsFactory)
)
when:
server.processRequest(Chain.ETHEREUM, Mono.just("".bytes), handler)
handler.execute(Chain.ETHEREUM, [reqItem], accessHandler)
.blockLast()
then:
1 * handler.onRequest(req)
1 * handler.onResponse(respItem)
1 * accessHandler.onRequest(req)
1 * accessHandler.onResponse(respItem)
}
def "Return error on invalid request"() {
setup:
ReadRpcJson read = Mock(ReadRpcJson) {
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.NumberId(4)) }
}
def handler = new HttpHandler(
read, new WriteRpcJson(),
Stub(NativeCall), Stub(AccessHandlerHttp.HandlerFactory), Stub(ProxyServer.RequestMetricsFactory)
)
when:
def act = handler.processRequest(Chain.ETHEREUM, Mono.just("".bytes), new AccessHandlerHttp.NoOpHandler())
.map { new String(it.array()) }
then:
StepVerifier.create(act)
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
.expectComplete()
.verify(Duration.ofSeconds(1))
}
def "Uses NativeCall"() {
setup:
NativeCall nativeCall = Mock(NativeCall)
def predefined = { a -> Flux.just("hello") } as Function
WriteRpcJson writeRpcJson = Mock {
1 * toJsons(_) >> predefined
}
def handler = new HttpHandler(
new ReadRpcJson(), writeRpcJson,
nativeCall, Stub(AccessHandlerHttp.HandlerFactory), Stub(ProxyServer.RequestMetricsFactory)
)
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
call.items.add(
BlockchainOuterClass.NativeCallItem.newBuilder()
.setMethod("eth_hello")
.build()
)
when:
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler())
then:
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
StepVerifier.create(act)
.expectNext("hello")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
}

View File

@@ -224,4 +224,12 @@ class ReadRpcJsonSpec extends Specification {
t.rpcMessage.toLowerCase() == "params must be an array"
t.details == new JsonRpcResponse.NumberId(2)
}
def "Error if json is broken"() {
when:
reader.apply('{"id":2, "method":"net_peerCount", "params"'.bytes)
then:
def t = thrown(RpcException)
t.code == -32700
}
}

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.proxy
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.rpc.NativeSubscribe
import io.emeraldpay.etherjar.rpc.json.RequestJson
import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.time.Duration
class WebsocketHandlerSpec extends Specification {
def requestHandler = new AccessHandlerHttp.NoOpHandler()
def "Parse standard RPC request"() {
setup:
def handler = new WebsocketHandler(
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
)
when:
def act = handler.parseRequest('{"id": 5, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x100001", false]}'.bytes)
.block(Duration.ofSeconds(1))
then:
act.id == 5
act.method == "eth_getBlockByNumber"
act.params == ["0x100001", false]
}
def "Parse to empty an invalid request"() {
setup:
def handler = new WebsocketHandler(
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
)
when:
def act = handler.parseRequest('hello world'.bytes)
.block(Duration.ofSeconds(1))
then:
act == null
}
def "Parse to empty a batch request"() {
setup:
def req1 = '{"id": 5, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x100001", false]}'
def handler = new WebsocketHandler(
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
)
when:
def act = handler.parseRequest("[$req1]".bytes)
.block(Duration.ofSeconds(1))
then:
act == null
}
def "Respond to a single call"() {
setup:
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
def nativeCall = Mock(NativeCall) {
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])
}
def handler = new WebsocketHandler(
new ReadRpcJson(), new WriteRpcJson(), nativeCall, Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
)
def request = new RequestJson("foo_test", [], 2)
when:
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler)
.single()
.block(Duration.ofSeconds(1))
then:
act == '{"jsonrpc":"2.0","id":2,"result":{"foo": 1}}'
}
def "Respond to a subscription call"() {
setup:
def response1 = [foo: 1]
def response2 = [foo: 2]
def nativeSubscribe = Mock(NativeSubscribe) {
1 * it.subscribe(Chain.ETHEREUM, "foo_test", null) >> Flux.fromIterable([response1, response2])
}
def handler = new WebsocketHandler(
new ReadRpcJson(), new WriteRpcJson(), Stub(NativeCall), nativeSubscribe, Stub(ProxyServer.RequestMetricsFactory)
)
def request = new RequestJson("eth_subscribe", ["foo_test"], 2)
when:
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler)
.collectList()
.block(Duration.ofSeconds(1))
then:
act[0] == '{"jsonrpc":"2.0","id":2,"result":"0000000000000001"}'
act[1] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":1},"subscription":"0000000000000001"}}'
act[2] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":2},"subscription":"0000000000000001"}}'
act.size() == 3
}
}