problem: users want to verify that response are coming from their nodes

solution: edge node can sign received valued
Co-authored-by: Igor Artamonov <igor@artamonov.ru>
This commit is contained in:
Vyacheslav Shebanov
2022-05-27 06:00:01 +03:00
committed by GitHub
parent 530811d272
commit d1ad77a345
74 changed files with 1325 additions and 291 deletions

View File

@@ -21,14 +21,19 @@ import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.OpenSsl
import io.netty.handler.ssl.OpenSslServerContext
import io.netty.handler.ssl.SslContext
import org.bouncycastle.jce.provider.BouncyCastleProvider
import spock.lang.Specification
import sun.security.x509.X509CertImpl
import java.security.Security
class TlsSetupSpec extends Specification {
TlsSetup tlsSetup = new TlsSetup(new FileResolver(new File("src/test/resources/tls-local")))
def setup() {
def setupSpec() {
Security.addProvider(new BouncyCastleProvider())
// !!!!!!!!!!!!
// run test on OS with OpenSSL installed
// !!!!!!!!!!!!
@@ -148,12 +153,13 @@ class TlsSetupSpec extends Specification {
def config = new AuthConfig.ServerTlsAuth(
enabled: true,
certificate: "127.0.0.1.crt",
key: "127.0.0.1.key",
// note that JDK Security doesn't accept non-P8 keys, but with Bouncy Castle we should test with a really invalid key
key: "127.0.0.1.invalid.key",
)
when:
tlsSetup.setupServer("test", config, false)
then:
def t = thrown(IllegalArgumentException)
thrown(Exception)
}
def "Fail if client certificate not set but required"() {

View File

@@ -0,0 +1,47 @@
package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.test.TestingCommons
import org.bouncycastle.util.io.pem.PemObject
import org.bouncycastle.util.io.pem.PemWriter
import spock.lang.Specification
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
class SignatureConfigReaderSpec extends Specification {
def "Parse enabled"() {
setup:
def config = "signed-response:\n" +
" enabled: true\n" +
" algorithm: SECP256K1\n" +
" private-key: /root/key.pem\n"
when:
def reader = new SignatureConfigReader(TestingCommons.fileResolver())
def act = reader.read(new ByteArrayInputStream(config.bytes))
then:
act.enabled
act.privateKey == "/root/key.pem"
act.algorithm == SignatureConfig.Algorithm.SECP256K1
}
def "No path when disabled"() {
setup:
def config = "signed-response:\n" +
" enabled: false\n" +
" private-key: /root/key.pem\n"
when:
def reader = new SignatureConfigReader(TestingCommons.fileResolver())
def act = reader.read(new ByteArrayInputStream(config.bytes))
then:
!act.enabled
act.privateKey == null
}
}

View File

@@ -62,7 +62,7 @@ class BaseHandlerSpec extends Specification {
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)
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
.collectList()
@@ -85,7 +85,7 @@ class BaseHandlerSpec extends Specification {
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)
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
.collectList()
@@ -116,8 +116,8 @@ class BaseHandlerSpec extends Specification {
call.items.add(request2)
call.ids[1] = 6
def response = [
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
]
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
@@ -149,8 +149,8 @@ class BaseHandlerSpec extends Specification {
call.items.add(request2)
call.ids[1] = 6
def response = [
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
]
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
@@ -189,8 +189,8 @@ class BaseHandlerSpec extends Specification {
// note there is only 2 responses
def response = [
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
new NativeCall.CallResult(2, '{"foo": 3}'.bytes, null)
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
new NativeCall.CallResult(2, null, '{"foo": 3}'.bytes, null, null)
]
when:
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))

View File

@@ -43,7 +43,7 @@ class HttpHandlerSpec extends Specification {
.setMethod("test_test")
.setPayload(ByteString.copyFromUtf8("[]"))
.build()
def respItem = new NativeCall.CallResult(1, "100".bytes, null)
def respItem = new NativeCall.CallResult(1, null, "100".bytes, null, null)
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
.addItems(reqItem)
@@ -129,7 +129,7 @@ class HttpHandlerSpec extends Specification {
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler(), false)
then:
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, null, "".bytes, null, null))
StepVerifier.create(act)
.expectNext("hello")
.expectComplete()

View File

@@ -84,7 +84,7 @@ class WebsocketHandlerSpec extends Specification {
def "Respond to a single call"() {
setup:
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
def nativeCall = Mock(NativeCall) {
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])

View File

@@ -85,7 +85,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 105
def data = [
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null)
]
when:
def act = writer.toJson(call, data[0])
@@ -98,7 +98,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
def data = [
new NativeCall.CallResult(1, null, new NativeCall.CallError(1, "Internal Error", null))
new NativeCall.CallResult(1, null, null, new NativeCall.CallError(1, "Internal Error", null), null)
]
when:
def act = writer.toJson(call, data[0])
@@ -111,7 +111,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = "aaa"
def data = [
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null)
]
when:
def act = writer.toJson(call, data[0])
@@ -126,9 +126,9 @@ class WriteRpcJsonSpec extends Specification {
call.ids[2] = 11
call.ids[3] = 15
def data = [
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null),
new NativeCall.CallResult(2, null, new NativeCall.CallError(2, "oops", null)),
new NativeCall.CallResult(3, '{"hash": "0x2484f459dc"}'.bytes, null),
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null),
new NativeCall.CallResult(2, null, null, new NativeCall.CallError(2, "oops", null), null),
new NativeCall.CallResult(3, null, '{"hash": "0x2484f459dc"}'.bytes, null, null),
]
when:
def act = Flux.fromIterable(data)
@@ -154,7 +154,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 10
def data = [
new NativeCall.CallResult(1, '"0x1"'.bytes, null),
new NativeCall.CallResult(1, null, '"0x1"'.bytes, null, null),
]
when:
def act = Flux.fromIterable(data)

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import spock.lang.Specification
class AlwaysQuorumSpec extends Specification {
@@ -26,7 +27,7 @@ class AlwaysQuorumSpec extends Specification {
def quorum = new AlwaysQuorum()
def up = Stub(Upstream)
when:
quorum.record(new JsonRpcException(1, "test"), up)
quorum.record(new JsonRpcException(1, "test"), null, up)
then:
quorum.isFailed()
!quorum.isResolved()
@@ -41,10 +42,11 @@ class AlwaysQuorumSpec extends Specification {
def quorum = new AlwaysQuorum()
def up = Stub(Upstream)
when:
quorum.record("123".bytes, up)
quorum.record("123".bytes, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
then:
quorum.isResolved()
quorum.getResult() == "123".bytes
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
!quorum.isFailed()
}
}

View File

@@ -43,21 +43,21 @@ class BroadcastQuorumSpec extends Specification {
!q.isResolved()
when:
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1)
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream1)
then:
!q.isResolved()
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
when:
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2)
then:
!q.isResolved()
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
when:
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
then:
1 * q.recordError(_, _, _)
1 * q.recordError(_, _, _, _)
q.isResolved()
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
}
@@ -75,21 +75,21 @@ class BroadcastQuorumSpec extends Specification {
!q.isResolved()
when:
q.record(new JsonRpcException(1, "Internal error"), upstream1)
q.record(new JsonRpcException(1, "Internal error"), null, upstream1)
then:
!q.isResolved()
1 * q.recordError(_, _, _)
1 * q.recordError(_, _, _, _)
when:
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2)
then:
!q.isResolved()
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
when:
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
then:
1 * q.recordError(_, _, _)
1 * q.recordError(_, _, _, _)
q.isResolved()
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
}
@@ -99,19 +99,19 @@ class BroadcastQuorumSpec extends Specification {
def quorum = new BroadcastQuorum(3)
def up = Stub(Upstream)
when:
quorum.record(new JsonRpcException(1, "test 1"), up)
quorum.record(new JsonRpcException(1, "test 1"), null, up)
then:
!quorum.isFailed()
!quorum.isResolved()
when:
quorum.record(new JsonRpcException(1, "test 2"), up)
quorum.record(new JsonRpcException(1, "test 2"), null, up)
then:
!quorum.isFailed()
!quorum.isResolved()
when:
quorum.record(new JsonRpcException(1, "test 3"), up)
quorum.record(new JsonRpcException(1, "test 3"), null, up)
then:
quorum.isFailed()
!quorum.isResolved()

View File

@@ -38,22 +38,23 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream2)
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream3)
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
then:
q.isFailed()
!q.isResolved()
q.signature == null
}
def "Fail first if not error"() {
@@ -70,7 +71,7 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed()
when:
q.record('"0x11"'.bytes, upstream1)
q.record('"0x11"'.bytes, null, upstream1)
then:
q.isResolved()
!q.isFailed()
@@ -90,14 +91,14 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
then:
!q.isFailed()
!q.isResolved()
q.signature == null
when:
q.record('"0x11"'.bytes, upstream2)
q.record('"0x11"'.bytes, null, upstream2)
then:
q.isResolved()
!q.isFailed()
@@ -117,14 +118,14 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed()
when:
q.record('null'.bytes, upstream2)
q.record('null'.bytes, null, upstream2)
then:
!q.isFailed()
!q.isResolved()
when:
q.record('"0x11"'.bytes, upstream2)
q.record('"0x11"'.bytes, null, upstream2)
then:
q.isResolved()
!q.isFailed()

View File

@@ -43,21 +43,21 @@ class NonceQuorumSpec extends Specification {
!q.isResolved()
when:
q.record('"0x10"'.bytes, upstream1)
q.record('"0x10"'.bytes, null, upstream1)
then:
!q.isResolved()
1 * q.recordValue(_, "0x10", _)
1 * q.recordValue(_, "0x10", _, _)
when:
q.record('"0x11"'.bytes, upstream2)
q.record('"0x11"'.bytes, null, upstream2)
then:
!q.isResolved()
1 * q.recordValue(_, "0x11", _)
1 * q.recordValue(_, "0x11", _, _)
when:
q.record('"0x10"'.bytes, upstream3)
q.record('"0x10"'.bytes, null, upstream3)
then:
1 * q.recordValue(_, "0x10", _)
1 * q.recordValue(_, "0x10", _, _)
q.isResolved()
objectMapper.readValue(q.result, Object) == "0x11"
}
@@ -75,27 +75,27 @@ class NonceQuorumSpec extends Specification {
!q.isResolved()
when:
q.record(new JsonRpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
then:
!q.isResolved()
1 * q.recordError(_, _, _)
1 * q.recordError(_, _, _, _)
when:
q.record('"0x11"'.bytes, upstream2)
q.record('"0x11"'.bytes, null, upstream2)
then:
!q.isResolved()
1 * q.recordValue(_, "0x11", _)
1 * q.recordValue(_, "0x11", _, _)
when:
q.record('"0x10"'.bytes, upstream3)
q.record('"0x10"'.bytes, null, upstream3)
then:
1 * q.recordValue(_, "0x10", _)
1 * q.recordValue(_, "0x10", _, _)
!q.isResolved()
when:
q.record('"0x11"'.bytes, upstream1)
q.record('"0x11"'.bytes, null, upstream1)
then:
1 * q.recordValue(_, "0x11", _)
1 * q.recordValue(_, "0x11", _, _)
q.isResolved()
objectMapper.readValue(q.result, Object) == "0x11"
}
@@ -114,23 +114,24 @@ class NonceQuorumSpec extends Specification {
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream2)
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new JsonRpcException(1, "Internal"), upstream3)
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
then:
q.isFailed()
!q.isResolved()
q.getError() != null
q.getError().message == "Internal"
q.signature == null
}
}

View File

@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.etherjar.rpc.RpcException
import spock.lang.Specification
@@ -31,7 +32,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, up)
quorum.record(value, null, up)
then:
1 * up.getLag() >> 0
quorum.isResolved()
@@ -39,6 +40,22 @@ class NotLaggingQuorumSpec extends Specification {
quorum.result == value
}
def "Keeps signature"() {
setup:
def up = Mock(Upstream)
def value = "foo".getBytes()
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
then:
1 * up.getLag() >> 0
quorum.isResolved()
!quorum.isFailed()
quorum.result == value
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
}
def "Resolves if ok lag"() {
setup:
def up = Mock(Upstream)
@@ -46,7 +63,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, up)
quorum.record(value, null, up)
then:
1 * up.getLag() >> 1
quorum.isResolved()
@@ -61,7 +78,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, up)
quorum.record(value, null, up)
then:
1 * up.getLag() >> 2
!quorum.isResolved()
@@ -75,7 +92,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(new JsonRpcException(-100, "test error"), up)
quorum.record(new JsonRpcException(-100, "test error"), null, up)
then:
1 * up.getLag() >> 1
!quorum.isResolved()

View File

@@ -15,7 +15,7 @@
*/
package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import org.jetbrains.annotations.NotNull
@@ -66,12 +66,14 @@ class ValueAwareQuorumSpec extends Specification {
}
@Override
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @NotNull Upstream upstream) {
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) {
}
@Override
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @NotNull Upstream upstream) {
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) {
}
@@ -85,6 +87,11 @@ class ValueAwareQuorumSpec extends Specification {
return false
}
@Override
ResponseSigner.Signature getSignature() {
return null
}
@Override
byte[] getResult() {
return new byte[0]

View File

@@ -31,11 +31,11 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.grpc.Chain
import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError
@@ -51,6 +51,16 @@ class NativeCallSpec extends Specification {
ObjectMapper objectMapper = Global.objectMapper
def nativeCall(MultistreamHolder upstreams = null, ResponseSigner signer = null) {
if (upstreams == null) {
upstreams = Stub(MultistreamHolder)
}
if (signer == null) {
signer = Stub(ResponseSigner)
}
new NativeCall(upstreams, signer)
}
def "Tries router first"() {
def routedApi = Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
@@ -58,11 +68,10 @@ class NativeCallSpec extends Specification {
def upstream = Mock(Multistream) {
1 * getRoutedApi(_) >> Mono.just(routedApi)
}
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(
1, upstream, Selector.empty, new AlwaysQuorum(),
1, null, upstream, Selector.empty, new AlwaysQuorum(),
new NativeCall.ParsedCallDetails("eth_test", [])
)
@@ -72,6 +81,7 @@ class NativeCallSpec extends Specification {
act.result == "1".bytes
}
def "Return error if router denied the requests"() {
def routedApi = Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message"))
@@ -79,11 +89,10 @@ class NativeCallSpec extends Specification {
def upstream = Mock(Multistream) {
1 * getRoutedApi(_) >> Mono.just(routedApi)
}
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(
15, upstream, Selector.empty, new AlwaysQuorum(),
15, null, upstream, Selector.empty, new AlwaysQuorum(),
new NativeCall.ParsedCallDetails("eth_test", [])
)
@@ -102,13 +111,13 @@ class NativeCallSpec extends Specification {
setup:
def quorum = new AlwaysQuorum()
def nativeCall = new NativeCall(Stub(MultistreamHolder))
def nativeCall = nativeCall()
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1))
1 * create(_, _, _) >> Mock(Reader) {
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, null, 1))
}
}
def call = new NativeCall.ValidCallContext(1, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", []))
when:
@@ -116,19 +125,20 @@ class NativeCallSpec extends Specification {
def act = objectMapper.readValue(resp.result, Object)
then:
act == "foo"
resp.nonce == 10
}
def "Returns error if no quorum"() {
setup:
def quorum = new AlwaysQuorum()
def nativeCall = new NativeCall(Stub(MultistreamHolder))
def nativeCall = nativeCall()
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * read(_) >> Mono.empty()
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.empty()
}
}
def call = new NativeCall.ValidCallContext(1, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_test", []))
when:
@@ -136,7 +146,7 @@ class NativeCallSpec extends Specification {
then:
StepVerifier.create(resp)
.expectNextMatches { result ->
result.isError()
result.isError() && result.nonce == 10
}
.expectComplete()
.verify(Duration.ofSeconds(1))
@@ -144,8 +154,7 @@ class NativeCallSpec extends Specification {
def "Packs call exception into response with id"() {
setup:
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
when:
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
then:
@@ -161,8 +170,7 @@ class NativeCallSpec extends Specification {
def "Packs unknown exception into response"() {
setup:
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
when:
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
then:
@@ -177,13 +185,12 @@ class NativeCallSpec extends Specification {
def "Builds normal response"() {
setup:
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
def json = [jsonrpc:"2.0", id:1, result: "foo"]
when:
def resp = nativeCall.buildResponse(
new NativeCall.CallResult(1561, objectMapper.writeValueAsBytes(json), null)
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, null)
)
then:
resp.id == 1561
@@ -191,10 +198,28 @@ class NativeCallSpec extends Specification {
objectMapper.readValue(resp.payload.toByteArray(), Map.class) == [jsonrpc:"2.0", id:1, result: "foo"]
}
def "Builds response with signature"() {
setup:
def nativeCall = nativeCall()
def json = [jsonrpc:"2.0", id:1, result: "foo"]
when:
def resp = nativeCall.buildResponse(
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, new ResponseSigner.Signature("sig1".bytes, "test", 100))
)
then:
resp.id == 1561
resp.succeed
resp.signature.nonce == 10
resp.signature.signature.toByteArray() == "sig1".bytes
resp.signature.keyId == 100
resp.signature.upstreamId == "test"
objectMapper.readValue(resp.payload.toByteArray(), Map.class) == [jsonrpc:"2.0", id:1, result: "foo"]
}
def "Returns error for invalid chain"() {
setup:
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChainValue(0)
@@ -210,7 +235,6 @@ class NativeCallSpec extends Specification {
then:
StepVerifier.create(resp)
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 0})
// .expectComplete()
.verify(Duration.ofSeconds(1))
}
@@ -219,7 +243,7 @@ class NativeCallSpec extends Specification {
def upstreams = Mock(MultistreamHolder) {
_ * it.observeChains() >> Flux.empty()
}
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall(upstreams)
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChainValue(Chain.TESTNET_MORDEN.id)
@@ -245,13 +269,14 @@ class NativeCallSpec extends Specification {
def upstreams = Mock(MultistreamHolder) {
_ * it.observeChains() >> Flux.empty()
}
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall(upstreams)
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
.addItems(
BlockchainOuterClass.NativeCallItem.newBuilder()
.setId(1)
.setNonce(10)
.setMethod("eth_test")
.setPayload(ByteString.copyFromUtf8("[]"))
)
@@ -263,6 +288,7 @@ class NativeCallSpec extends Specification {
act.size() == 1
with(act[0]) {
id == 1
nonce == 10
payload.method == "eth_test"
payload.params == "[]"
}
@@ -273,7 +299,7 @@ class NativeCallSpec extends Specification {
def upstreams = Mock(MultistreamHolder) {
_ * it.observeChains() >> Flux.empty()
}
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall(upstreams)
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
@@ -300,7 +326,7 @@ class NativeCallSpec extends Specification {
def upstreams = Mock(MultistreamHolder) {
_ * it.observeChains() >> Flux.empty()
}
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall(upstreams)
def item = BlockchainOuterClass.NativeCallItem.newBuilder()
.setId(1)
@@ -338,7 +364,7 @@ class NativeCallSpec extends Specification {
def multistreamHolder = Mock(MultistreamHolder) {
_ * it.observeChains() >> Flux.empty()
}
def nativeCall = new NativeCall(multistreamHolder)
def nativeCall = nativeCall(multistreamHolder)
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
@@ -365,8 +391,8 @@ class NativeCallSpec extends Specification {
def "Parse empty params"() {
setup:
def nativeCall = new NativeCall(Stub(MultistreamHolder))
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
new NativeCall.RawCallDetails("eth_test", "[]"))
when:
def act = nativeCall.parseParams(ctx)
@@ -378,8 +404,8 @@ class NativeCallSpec extends Specification {
def "Parse none params"() {
setup:
def nativeCall = new NativeCall(Stub(MultistreamHolder))
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
new NativeCall.RawCallDetails("eth_test", ""))
when:
def act = nativeCall.parseParams(ctx)
@@ -391,8 +417,8 @@ class NativeCallSpec extends Specification {
def "Parse single param"() {
setup:
def nativeCall = new NativeCall(Stub(MultistreamHolder))
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
new NativeCall.RawCallDetails("eth_test", "[false]"))
when:
def act = nativeCall.parseParams(ctx)
@@ -404,8 +430,8 @@ class NativeCallSpec extends Specification {
def "Parse multi param"() {
setup:
def nativeCall = new NativeCall(Stub(MultistreamHolder))
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
new NativeCall.RawCallDetails("eth_test", "[false, 123]"))
when:
def act = nativeCall.parseParams(ctx)
@@ -419,12 +445,11 @@ class NativeCallSpec extends Specification {
//TODO
def "Calls cache before remote"() {
setup:
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
def api = TestingCommons.api()
def upstream = TestingCommons.multistream(api)
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10,
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10, null,
upstream,
Selector.empty, new AlwaysQuorum(),
new NativeCall.ParsedCallDetails("eth_test", []))
@@ -438,11 +463,10 @@ class NativeCallSpec extends Specification {
//TODO
def "Uses cached value"() {
setup:
def upstreams = Stub(MultistreamHolder)
def nativeCall = new NativeCall(upstreams)
def nativeCall = nativeCall()
def upstream = TestingCommons.multistream(TestingCommons.api())
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10,
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10, null,
upstream,
Selector.empty, new AlwaysQuorum(),
new NativeCall.ParsedCallDetails("eth_test", []))

View File

@@ -91,7 +91,6 @@ class TrackEthereumTxSpec extends Specification {
.setBlock(
Common.BlockInfo.newBuilder()
.setHeight(blockJson.number)
.setWeight(ByteString.copyFrom(blockJson.totalDifficulty.toByteArray()))
.setBlockId(blockJson.hash.toHex().substring(2))
.setTimestamp(blockJson.timestamp.toEpochMilli())
).build()
@@ -280,7 +279,6 @@ class TrackEthereumTxSpec extends Specification {
.setBlock(
Common.BlockInfo.newBuilder()
.setHeight(blocks[2].number)
.setWeight(ByteString.copyFrom(blocks[2].totalDifficulty.toByteArray()))
.setBlockId(blocks[2].hash.toHex().substring(2))
.setTimestamp(blocks[2].timestamp.toEpochMilli())
)

View File

@@ -108,7 +108,7 @@ class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
}
error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked")
}
return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id))
return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id), null)
} as Callable<JsonRpcResponse>
return Mono.fromCallable(call)
}
@@ -323,7 +323,7 @@ class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
}
@Override
def <S> NettyOutbound sendUsing(Callable<? extends S> sourceInput, BiFunction<? super Connection, ? super S, ?> mappedInput, Consumer<? super S> sourceCleanup) {
<S> NettyOutbound sendUsing(Callable<? extends S> sourceInput, BiFunction<? super Connection, ? super S, ?> mappedInput, Consumer<? super S> sourceCleanup) {
return this
}

View File

@@ -214,7 +214,7 @@ class FilteredApisSpec extends Specification {
setup:
List<Upstream> standard = (0..1).collect {
TestingCommons.upstream(
it.toString(),
"test_" + it,
new EthereumApiStub(it)
)
}
@@ -246,7 +246,7 @@ class FilteredApisSpec extends Specification {
setup:
List<Upstream> standard = (0..1).collect {
TestingCommons.upstream(
it.toString(),
"test_" + it,
new EthereumApiStub(it)
)
}

View File

@@ -68,8 +68,8 @@ class MultistreamSpec extends Specification {
def "Filter Best Status accepts better input"() {
setup:
def up1 = TestingCommons.upstream("1")
def up2 = TestingCommons.upstream("2")
def up1 = TestingCommons.upstream("test-1")
def up2 = TestingCommons.upstream("test-2")
def time0 = Instant.now() - Duration.ofSeconds(60)
def filter = new Multistream.FilterBestAvailability()
def update0 = new Multistream.UpstreamStatus(
@@ -87,8 +87,8 @@ class MultistreamSpec extends Specification {
def "Filter Best Status declines worse input"() {
setup:
def up1 = TestingCommons.upstream("1")
def up2 = TestingCommons.upstream("2")
def up1 = TestingCommons.upstream("test-1")
def up2 = TestingCommons.upstream("test-2")
def time0 = Instant.now() - Duration.ofSeconds(60)
def filter = new Multistream.FilterBestAvailability()
def update0 = new Multistream.UpstreamStatus(
@@ -106,7 +106,7 @@ class MultistreamSpec extends Specification {
def "Filter Best Status accepts worse input from same upstream"() {
setup:
def up = TestingCommons.upstream("1")
def up = TestingCommons.upstream("test-1")
def time0 = Instant.now() - Duration.ofSeconds(60)
def filter = new Multistream.FilterBestAvailability()
def update0 = new Multistream.UpstreamStatus(
@@ -124,8 +124,8 @@ class MultistreamSpec extends Specification {
def "Filter Best Status accepts any input if existing is outdated"() {
setup:
def up1 = TestingCommons.upstream("1")
def up2 = TestingCommons.upstream("2")
def up1 = TestingCommons.upstream("test-1")
def up2 = TestingCommons.upstream("test-2")
def time0 = Instant.now() - Duration.ofSeconds(90)
def filter = new Multistream.FilterBestAvailability()
def update0 = new Multistream.UpstreamStatus(
@@ -143,9 +143,9 @@ class MultistreamSpec extends Specification {
def "Filter Best Status declines same status"() {
setup:
def up1 = TestingCommons.upstream("1")
def up2 = TestingCommons.upstream("2")
def up3 = TestingCommons.upstream("3")
def up1 = TestingCommons.upstream("test-1")
def up2 = TestingCommons.upstream("test-2")
def up3 = TestingCommons.upstream("test-3")
def time0 = Instant.now() - Duration.ofSeconds(60)
def filter = new Multistream.FilterBestAvailability()
def update0 = new Multistream.UpstreamStatus(
@@ -172,7 +172,7 @@ class MultistreamSpec extends Specification {
def "Call postprocess after api use"() {
setup:
def request = new JsonRpcRequest("test_foo", [1], 1)
def request = new JsonRpcRequest("test_foo", [1], 1, null)
def api = TestingCommons.api()
api.answer("test_foo", [1], "test")

View File

@@ -13,7 +13,7 @@ class RequestPostprocessorSpec extends Specification {
def "Wrappers calls onReceive for a value"() {
setup:
def request = new JsonRpcRequest("test_foo", [1], 1)
def request = new JsonRpcRequest("test_foo", [1], 1, null)
def processor = Mock(RequestPostprocessor)
def api = TestingCommons.api()
api.answer("test_foo", [1], "test")
@@ -30,7 +30,7 @@ class RequestPostprocessorSpec extends Specification {
def "Wrappers doesn't call onReceive for no value"() {
setup:
def request = new JsonRpcRequest("test_foo", [1], 1)
def request = new JsonRpcRequest("test_foo", [1], 1, null)
def processor = Mock(RequestPostprocessor)
Reader<JsonRpcRequest, JsonRpcResponse> reader = Mock(Reader) {
1 * it.read(request) >> Mono.empty()

View File

@@ -50,10 +50,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), 1
Global.objectMapper.writeValueAsBytes(json), null, 1
)
)
}
@@ -81,10 +81,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(null), 1
Global.objectMapper.writeValueAsBytes(null), null, 1
)
)
}
@@ -116,10 +116,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), 1
Global.objectMapper.writeValueAsBytes(json), null, 1
)
)
}
@@ -152,10 +152,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), 1
Global.objectMapper.writeValueAsBytes(json), null, 1
)
)
}
@@ -183,10 +183,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(null), 1
Global.objectMapper.writeValueAsBytes(null), null, 1
)
)
}
@@ -214,10 +214,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes("0x100"), 1
Global.objectMapper.writeValueAsBytes("0x100"), null, 1
)
)
}
@@ -246,10 +246,10 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls
)
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _) >> Mock(Reader) {
1 * create(_, _, _) >> Mock(Reader) {
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just(
new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes("0x100"), 1
Global.objectMapper.writeValueAsBytes("0x100"), null, 1
)
)
}

View File

@@ -37,6 +37,25 @@ class LocalCallRouterSpec extends Specification {
act.resultAsProcessedString == "0x0000000000000000000000000000000000000000"
}
def "Returns empty if nonce set"() {
setup:
def methods = new DefaultEthereumMethods(Chain.ETHEREUM)
def router = new LocalCallRouter(
new EthereumReader(
TestingCommons.multistream(TestingCommons.api()),
Caches.default(),
ConstantFactory.constantFactory(new DefaultEthereumMethods(Chain.ETHEREUM))
),
methods,
new EmptyHead()
)
when:
def act = router.read(new JsonRpcRequest("eth_getTransactionByHash", ["test"], 10))
.block(Duration.ofSeconds(1))
then:
act == null
}
def "getBlockByNumber with latest uses latest id"() {
setup:
def head = Mock(Head) {

View File

@@ -83,7 +83,7 @@ class WsConnectionSpec extends Specification {
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null))
then:
StepVerifier.create(act)
@@ -105,7 +105,7 @@ class WsConnectionSpec extends Specification {
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null))
then:
StepVerifier.create(act)
@@ -129,7 +129,7 @@ class WsConnectionSpec extends Specification {
when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null))
then:
StepVerifier.create(act)

View File

@@ -30,7 +30,7 @@ class JsonRpcResponseSpec extends Specification {
when:
def act = resp1.equals(resp2)
then:
act == true
act
}
def "Extract processed string without quoted"() {
@@ -49,7 +49,7 @@ class JsonRpcResponseSpec extends Specification {
def "Fails to extract processed string if not quoted"() {
when:
def act = new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString
new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString
then:
thrown(IllegalStateException)
}
@@ -63,7 +63,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and null result"() {
setup:
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.NumberId(1))
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.NumberId(1), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -72,7 +72,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and string result"() {
setup:
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.NumberId(10))
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.NumberId(10), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -81,7 +81,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and object result"() {
setup:
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.NumberId(101))
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.NumberId(101), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and error"() {
setup:
def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.NumberId(101))
def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.NumberId(101), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -99,7 +99,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and null result"() {
setup:
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"))
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -108,7 +108,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and string result"() {
setup:
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"))
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -117,7 +117,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and object result"() {
setup:
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"))
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"), null)
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -128,7 +128,7 @@ class JsonRpcResponseSpec extends Specification {
setup:
def json = new JsonRpcResponse(null,
new JsonRpcError(-32041, "Oooops"),
new JsonRpcResponse.StringId("9kbo29gkaasf"))
new JsonRpcResponse.StringId("9kbo29gkaasf"), null)
when:
def act = objectMapper.writeValueAsString(json)
then:

View File

@@ -0,0 +1,28 @@
package io.emeraldpay.dshackle.upstream.signature
import io.emeraldpay.dshackle.config.SignatureConfig
import spock.lang.Specification
class ResponseSignerFactorySpec extends Specification {
def "No signer if not enabled"() {
setup:
def conf = new SignatureConfig()
when:
def signer = new ResponseSignerFactory(conf).getObject()
then:
signer instanceof NoSigner
}
def "No signer if privkey is not configured"() {
setup:
def conf = new SignatureConfig()
when:
def signer = new ResponseSignerFactory(conf).getObject()
then:
signer instanceof NoSigner
}
}

View File

@@ -0,0 +1,138 @@
package io.emeraldpay.dshackle.upstream.signature
import io.emeraldpay.dshackle.config.SignatureConfig
import io.emeraldpay.dshackle.config.SignatureConfigReader
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Upstream
import org.apache.commons.codec.binary.Hex
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.util.io.pem.PemObject
import org.bouncycastle.util.io.pem.PemWriter
import spock.lang.Specification
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Security
import java.security.Signature
import java.security.interfaces.ECPrivateKey
import java.security.spec.ECGenParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
class Secp256KSignerSpec extends Specification {
def setupSpec() {
Security.addProvider(new BouncyCastleProvider())
}
def "Reads private key"() {
setup:
def file = File.createTempFile("test", ".pem")
def keygen = KeyPairGenerator.getInstance("EC")
keygen.initialize(new ECGenParameterSpec("secp256k1"))
def key = keygen.generateKeyPair()
def keyBuilder = new PKCS8EncodedKeySpec(key.getPrivate().getEncoded())
def writer = new PemWriter(new FileWriter(file.path))
writer.writeObject(new PemObject("PRIVATE KEY", keyBuilder.getEncoded()))
writer.close()
when:
def signer = new ResponseSignerFactory(new SignatureConfig())
def act = signer.readKey(SignatureConfig.Algorithm.SECP256K1, file.absolutePath).first
then:
act == key.getPrivate()
cleanup:
file.delete()
}
def "Id is a hash of x509 public key"() {
setup:
def conf = new SignatureConfig()
conf.enabled = true
conf.privateKey = "testing/dshackle/test_key"
def signer = new ResponseSignerFactory(conf).getObject() as Secp256KSigner
// To verify the test, check the hash of test key above:
//
// echo MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE3zetdMdyTO/sTFCLeOrI5moiZt2RjfUVdavhorgqd+gxAqM01cf5Q4QZ8INne9RykcQsbLYXQfDXJbGMm5+gdg== | base64 -d - | shasum -a 256
// d25f1ff2c1a57235a9bc7725cd645ab0e9631475a12402f2881579d3f6887597 -
//
when:
def id = signer.keyId
then:
id == 0xd25f1ff2c1a57235L
}
def "Wrap message"() {
setup:
def up = Mock(Upstream) {
_ * getId() >> "infura"
}
def signer = new Secp256KSigner(Stub(ECPrivateKey), 100L)
when:
def act = signer.wrapMessage(10, "test".bytes, up)
then:
act == "DSHACKLESIG/10/infura/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
def "Signed message is valid"() {
setup:
def result = "test".bytes
def up = Mock(Upstream) {
_ * getId() >> "infura"
}
def keyPairGen = KeyPairGenerator.getInstance("EC")
keyPairGen.initialize(new ECGenParameterSpec("secp256k1"))
def pair = keyPairGen.generateKeyPair()
def verifier = Signature.getInstance("SHA256withECDSA")
verifier.initVerify(pair.getPublic())
verifier.update("DSHACKLESIG/10/infura/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08".getBytes())
def signer = new Secp256KSigner((pair.getPrivate() as ECPrivateKey), 100L)
when:
def sig = signer.sign(10, result, up)
then:
verifier.verify(sig.value)
}
def "Signed message is valid - for docs"() {
// it's the example used in docs
setup:
def result = '["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", true]'.bytes
def up = Mock(Upstream) {
_ * getId() >> "infura"
}
def sha256 = MessageDigest.getInstance("SHA-256")
def conf = new SignatureConfig()
conf.enabled = true
conf.privateKey = "testing/dshackle/test_key"
def factory = new ResponseSignerFactory(conf)
def sk = factory.readKey(conf.algorithm, conf.privateKey).first
def pk = factory.extractPublicKey(KeyFactory.getInstance("EC"), sk)
def verifier = Signature.getInstance("SHA256withECDSA")
verifier.initVerify(pk)
verifier.update("DSHACKLESIG/10/infura/${Hex.encodeHexString(sha256.digest(result))}".getBytes())
def signer = factory.getObject() as Secp256KSigner
when:
def sig = signer.sign(10, result, up)
println("Signature: ${Hex.encodeHexString(sig.value)}")
then:
verifier.verify(sig.value)
}
}

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA0x1gOF8K9SFwQ9wohBe/EWsjLYdQ4z2yimW604BS/p1tM6xN
4JylRWC3rawllw+cTYjLWdgd5WX43u+6i9TG3Ni19bisZamuO2HitVxEeRY/DPlB
zIfobDGxSL/R0S1ug7HKqp/dbkL5XT4AlgZn9sj3ikYU4wLj7YdOQXyAIM5FYq5I
brtKqIP0cvQsOsR2mr4gk569bu386rP4NZw75UCLVPrbG6ngkv5YkjVQa0M94RPR
JGreRdvyfi17nxjo1ePKh1iWif4ERJjjYk/DhTDquNyYMdBWmGDkO2Xfjc5x830I
16G1dq+6zNB1FT25jVFR1Wy+DjjGFHrSG4UnzQIDAQABAoIBAQCX1uj9ol4fMI2u
QQpi9zFVNdl3RXvH9PgU0lYtCH6o4lFIeQUKJ6A25fk10Dq5C2E/4sNfOzFFbLIy
pfll2QOuk69LrCdSd1f5Hc4Q4uvcq0Nt8ViB4r4oExWPXWdrK2HxFk7NqW15gHIZ
vh5tyO29cY2Yxg7/t3R3wnlmYEVHUcS7HmhzgDveNzA0VLza3765ntgwXypY8N2j
heEQC1h5kMCurcKJyRXmlsXPRWizX0UBWDrMHFeqyhrH0BlRSFTNC3sKmyYaJQmp
daPNRr4zO0yfm8utVSbNHX2OM5DpIO1Ecq9Sd43QI+ATAtxFrhPoYK1rwll267CV
cJCRbz+BAoGBAPzz/1Xq8s1lGZ6eWz7H1JlzxYLH+TzQfrV1ym7xJgR/b3rVXiJ+
D+qL8zUJDa5xZyflXB7zCg4I7ALmNwMJzVLdIOpEntHK2NtRJ2rp4qH+kMXojays
zOGYfbRLNVe+mgAK9Pu8eOi8NzXqkB/S8rml3xqSOpUFsvlc2qiYhGdXAoGBANWo
XcQDpisRFcrn3J0+pKU57ZIRjxyOTDlEwH7k+x+PprCRFki80kW22u4l22FdDaip
s4vCuAm5tmEogEjINU6ZhSKHxonjaGXfzuZ3gAMk/PN7zFazlgfYEKng+fa1YuZ+
3Ubzq6py8enoffJ/PSF/lClKlV5sxjyilxeZmOd7AoGBAMtaJHUf0l4I3tXDnLsV
4JKzLpjm3X7uwfNehH/Q0t9EVYKk9/BPYs4h1zywEYwesJNEO7p8w/7mAMSZc5AB
zvYmOixvMxEO1C5xKXKS7utCv45SJcE48vat17FVO+h3RmSuYKaI4BZ0Wbfi92q7
+BwDGx6zW+EdmcoaOba8FgU1AoGAV2WftWaoukUq3O0rWUceolenznBQUiYDGAn/
k+imsKpaTS+MJgTXHp1FwNTLgHBH/g4s26azEYdeCzA+CYecBqLVyuIvXIghVErQ
n4WSX7bpoc+qLm0Xme3QIy1cEobwBckvSq6yMe8C9eOcYW2a2/EL8jgIEa/9ByCb
HZQ+77ECgYEAxR1eoxc/XV9rcftdaRl7+Db9Qvnhfwu7MFQ3lgomol1N1ckSjwnO
wo/HX4+8cMS5QN11d8l2hf+7TyuRCrQBLrYYkrVWb4Z+ote3ejrAsDg90xjOFYWf
MWSy7kPeDl3JTEdNPFIIa28EQhZYupD0ihBoVspz2eQ1Y66BOfqC9nU=
-----END RSA PRIVATE KEY-----