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

@@ -49,6 +49,7 @@ message NativeCallItem {
uint32 id = 1;
string method = 3;
bytes payload = 4;
uint64 nonce = 5;
}
----
@@ -70,6 +71,14 @@ message NativeCallReplyItem {
bool succeed = 2;
bytes payload = 3;
bytes error = 4;
NativeCallReplySignature signature = 5;
}
message NativeCallReplySignature {
uint64 nonce = 1;
bytes signature = 2;
uint64 key_id = 3;
string upstream_id = 4;
}
----
@@ -79,7 +88,104 @@ Where:
- or `error` if request failed (`succeed` is false)
NOTE: Reply Items comes right after their execution on an upstream, therefore streaming response.
It allows to build non-blocking queries
It allows building non-blocking queries
[#signatures]
=== Signed JSON RPC Responses
Dshackle can sign the responses it received from an upstream.
It can be enabled on server by configuring a path to a Secp256K1 Key used for signing.
And passing a `nonce` as part of the call request.
When both of them are set Dshackle adds a Signature to the response, which can travel through multiple levels of Dshackle-Dshackle connections.
WARNING: Caching is disabled for signed requests, and you always touch an actual node even if there is a ready to use data in local cache.
The signed message looks like:
----
DSHACKLESIG/$nonce/$upstreamId/hex(sha256($response))
----
.Where
- `nonce` is a 64-bit number provided with the request
- `upstreamId` id of an upstream which produced the result
- `response` is a part of the original JSON RPC message, i.e. it's what you have in `payload` field of `NativeCallReplyItem`
.A signed response includes:
- `nonce` original nonce used for the call
- `signature` signature bytes (of the message above)
- `key_id` identifier of a key used for the signing
- `upstream_id` id of upstream which produce the response
==== How to generate a key
Here we generate a pair of Secret and Public keys using openssl.
[source, bash]
----
export KEYNAME=mykey
openssl ecparam -name secp256k1 -out $KEYNAME_param.pem
openssl ecparam -in $KEYNAME_param.pem -genkey -noout -out $KEYNAME.pem
openssl ec -in $KEYNAME.pem -text
openssl ec -in $KEYNAME.pem -out ${KEYNAME}_pub.pem -pubout
rm $KEYNAME_param.pem
cat ${KEYNAME}_pub.pem
----
As a result you get `mykey.pem` with secret key to use on server, and `mykey_pub.pem` with public key to use on client to verify signatures.
==== How to verify a signature
Here is the example how to verify the signature with command line, and it can be easily adapted for your language of choice.
.First, let's prepare all the values:
[source, bash]
----
export PUBKEY=testing/dshackle/test_key.pub
export NONCE=10
export UPSTREAM=infura
export PAYLOAD='["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", true]'
export SIGNATURE=3045022100be1d730e0e381e25bff64f0fc598d19e31688a01db751098d0ed21847ca785b0022002f5651a0e8d447b0815aeb7b48738cb470cf46d60ee4e2f5bc9c0dc4e072dc3
----
.Then rebuild the signed message:
[source, bash]
----
echo -n "DSHACKLESIG/$NONCE/$UPSTREAM/" > msg.txt
echo -n $PAYLOAD | shasum -a 256 - | awk '{ printf $1 }' >> msg.txt
----
.And save signature as a binary file:
[source, bash]
----
rm -f msg.sig && echo $SIGNATURE | xxd -r -p - msg.sig
----
.Now you can verify the payload with the following:
[source, bash]
----
openssl dgst -sha256 -verify $PUBKEY -signature msg.sig msg.txt
----
.Which should print:
----
Verified OK
----
==== What is the Key Identifier?
Key Id is the first 64 bits of SHA-256 hash of the x509 encoded Public Key.
It's provided with the Signed Response for a reference.
You can get it with:
[source, bash]
----
cat $PUBKEY | sed -e '$ d' | awk '(NR>1)' | base64 -d | shasum -a 256 - | head -c 16
----
=== Wrapped JSON RPC subscriptions

View File

@@ -47,6 +47,11 @@ cache:
db: 0
password: I1y0dGKy01by
signed-response:
enabled: true
algorithm: SECP256K1
private-key: /path/key.pem
proxy:
host: 0.0.0.0
port: 8080
@@ -199,6 +204,11 @@ See <<tokens>> section
| Caching configuration.
See <<cache>> section.
| `signed-response`
|
| Signed responses
See <<signed-response>> section.
| `cluster`
|
| Setup connection to remote nodes.See <<cluster>> section
@@ -546,6 +556,38 @@ cache:
|===
[#signed-response]
== Signed Response
[source,yaml]
----
signed-response:
enabled: true
algorithm: SECP256K1
private-key: /path/key.pem
----
.Redis Config
[cols="2a,2,5"]
|===
| Option | Default Value | Description
| `enabled`
| `false`
| Enable/disable Signed Responses
| `algorithm`
| `SECP256K1`
| SECP256K1 only possible at this moment
| `private-key`
|
| Path to a private key in PEM format
|===
See more details at link:07-methods.adoc#signatures[Signed Response] in gRPC Methods.
[#cluster]
== Cluster

View File

@@ -31,7 +31,7 @@ cglib-nodep = "cglib:cglib-nodep:3.3.0"
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
emerald-api = "io.emeraldpay:emerald-api:0.11.1"
emerald-api = "io.emeraldpay:emerald-api:0.12-alpha.1"
equals-verifier = "nl.jqno.equalsverifier:equalsverifier:3.3"

View File

@@ -34,6 +34,29 @@ message NativeCallItem {
uint32 id = 1;
string method = 3;
bytes payload = 4;
uint64 nonce = 5;
}
/**
* Signature for a response
*/
message NativeCallReplySignature {
/**
* Original nonce value used for the call
*/
uint64 nonce = 1;
/**
* Signature value
*/
bytes signature = 2;
/**
* Key Id used for the signing
*/
uint64 key_id = 3;
/**
* Id of the upstream produced the response
*/
string upstream_id = 4;
}
message NativeCallReplyItem {
@@ -41,6 +64,11 @@ message NativeCallReplyItem {
bool succeed = 2;
bytes payload = 3;
string errorMessage = 4;
/**
* Optional signature for the response.
* Available only when it's configured at the edge dshackle and nonce is provided wit the request.
*/
NativeCallReplySignature signature = 5;
}
message NativeSubscribeRequest {
@@ -231,7 +259,7 @@ enum FeeEstimationMode {
* Standard Ethereum Fee, supported by majority of forks and by Ethereum Mainnet before EIP-1559
*/
message EthereumStdFees {
// Fee value in Wei
// Big Number encoded as string. Fee value in Wei
string fee = 1;
}
@@ -239,11 +267,11 @@ message EthereumStdFees {
* Ethereum Fee for EIP-1559 compatible forks
*/
message EthereumExtFees {
// Estimated fee that would be actually paid. I.e. it's the Base Fee + Priority Fee
// Big Number encoded as string. Estimated fee that expected to be actually paid. I.e. it's the Base Fee + Priority Fee
string expect = 1;
// Priority Fee in Wei
// Big Number encoded as string. Priority Fee in Wei
string priority = 2;
// Max Fee value in Wei. Note that it only indicated current preference and actual Max may be significantly lower, depending on the usage scenario.
// Big Number encoded as string. Max Fee value in Wei. Note that it only indicates the current preference, and the actual Max may be significantly lower, depending on the usage scenario.
string max = 3;
}
@@ -252,5 +280,5 @@ message EthereumExtFees {
*/
message BitcoinStdFees {
// Fee in Satoshi per Kilobyte. Note that the actual fee calculation MUST divide it by 1024 at the last step to get a fair fee.
string satPerKb = 1;
uint64 satPerKb = 1;
}

View File

@@ -21,8 +21,10 @@ import io.emeraldpay.dshackle.config.HealthConfig
import io.emeraldpay.dshackle.config.MainConfig
import io.emeraldpay.dshackle.config.MainConfigReader
import io.emeraldpay.dshackle.config.MonitoringConfig
import io.emeraldpay.dshackle.config.SignatureConfig
import io.emeraldpay.dshackle.config.TokensConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
@@ -37,6 +39,7 @@ import org.springframework.scheduling.annotation.EnableScheduling
import reactor.core.scheduler.Scheduler
import reactor.core.scheduler.Schedulers
import java.io.File
import java.security.Security
import java.util.concurrent.Executors
import kotlin.system.exitProcess
@@ -66,6 +69,8 @@ open class Config(
it
}
}
Security.addProvider(BouncyCastleProvider())
}
fun getConfigPath(): File {
@@ -119,6 +124,11 @@ open class Config(
return mainConfig.cache ?: CacheConfig()
}
@Bean
open fun signatureConfig(@Autowired mainConfig: MainConfig): SignatureConfig {
return mainConfig.signature ?: SignatureConfig()
}
@Bean
open fun tokensConfig(@Autowired mainConfig: MainConfig): TokensConfig {
return mainConfig.tokens ?: TokensConfig(emptyList())

View File

@@ -26,4 +26,5 @@ class MainConfig {
var monitoring: MonitoringConfig = MonitoringConfig.default()
var accessLogConfig: AccessLogConfig = AccessLogConfig.default()
var health: HealthConfig = HealthConfig.default()
var signature: SignatureConfig? = null
}

View File

@@ -36,6 +36,7 @@ class MainConfigReader(
private val monitoringConfigReader = MonitoringConfigReader()
private val accessLogReader = AccessLogReader()
private val healthConfigReader = HealthConfigReader()
private val signatureConfigReader = SignatureConfigReader(fileResolver)
fun read(input: InputStream): MainConfig? {
val configNode = readNode(input)
@@ -75,6 +76,9 @@ class MainConfigReader(
healthConfigReader.read(input).let {
config.health = it
}
signatureConfigReader.read(input).let {
config.signature = it
}
return config
}
}

View File

@@ -0,0 +1,31 @@
package io.emeraldpay.dshackle.config
import java.util.Locale
class SignatureConfig {
enum class Algorithm {
SECP256K1
}
companion object {
fun algorithmOfString(algo: String): Algorithm {
val algorithm = when (algo.uppercase(Locale.getDefault())) {
"SECP256K1" -> Algorithm.SECP256K1
else -> throw IllegalArgumentException("Unknown algorithm or not allowed")
}
return algorithm
}
}
/**
* Signature scheme that we should use
*/
var algorithm: Algorithm = Algorithm.SECP256K1
/**
* Should we generate signature on this instance if it's not already present
*/
var enabled: Boolean = false
var privateKey: String? = null
}

View File

@@ -0,0 +1,40 @@
package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.FileResolver
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.nodes.MappingNode
import java.io.InputStream
class SignatureConfigReader(val fileResolver: FileResolver) : YamlConfigReader(), ConfigReader<SignatureConfig> {
companion object {
private val log = LoggerFactory.getLogger(SignatureConfig::class.java)
}
fun read(input: InputStream): SignatureConfig? {
val configNode = readNode(input)
return read(configNode)
}
override fun read(input: MappingNode?): SignatureConfig? {
return getMapping(input, "signed-response")?.let { node ->
val config = SignatureConfig()
getValueAsBool(node, "enabled")?.let {
config.enabled = it
}
if (config.enabled) {
getValueAsString(node, "algorithm")?.let {
config.algorithm = SignatureConfig.algorithmOfString(it)
}
getValueAsString(node, "private-key")?.let {
val key = fileResolver.resolve(it)
config.privateKey = key.absolutePath
}
}
if (config.enabled && config.privateKey == null) {
throw IllegalStateException("Path to a private key (`signature.private-key`) is required when Response signature is enabled.")
}
config
}
}
}

View File

@@ -183,6 +183,8 @@ class UpstreamsConfigReader(
fun isValid(upstream: UpstreamsConfig.Upstream<*>): Boolean {
val id = upstream.id
// In general, we just check that id is suitable for urls and references,
// Besides that, if Response Signatures are enabled (CurrentResponseSigner) then it's critical that the id cannot have `/` symbol
if (id == null || id.length < 3 || !id.matches(Regex("[a-zA-Z][a-zA-Z0-9_-]+[a-zA-Z0-9]"))) {
log.warn("Invalid id: $id")
return false

View File

@@ -110,7 +110,9 @@ class Events {
val payloadSizeBytes: Long,
val nativeCall: NativeCallItemDetails,
val responseBody: String? = null,
val errorMessage: String? = null
val errorMessage: String? = null,
val nonce: Long? = null,
val signature: String? = null
) : ChainBase(blockchain, "NativeCall", id, channel)
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -163,6 +165,7 @@ class Events {
val method: String,
val id: Int,
val payloadSizeBytes: Long,
val nonce: Long,
val requestParams: String? = null
)

View File

@@ -23,6 +23,7 @@ import io.grpc.Attributes
import io.grpc.Grpc
import io.grpc.Metadata
import io.netty.handler.codec.http.HttpHeaders
import org.apache.commons.codec.binary.Hex
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import reactor.netty.http.server.HttpServerRequest
@@ -312,6 +313,7 @@ class EventsBuilder {
item.method,
item.id,
item.payload.size().toLong(),
item.nonce,
if (accessLogConfig.includeMessages) {
if (item.payload != null && !item.payload.isEmpty && item.payload.isValidUtf8) item.payload.toStringUtf8() else ""
} else null
@@ -336,6 +338,8 @@ class EventsBuilder {
if (msg.payload != null && !msg.payload.isEmpty && msg.payload.isValidUtf8) msg.payload.toStringUtf8() else ""
} else null,
errorMessage = if (accessLogConfig.includeMessages) msg.errorMessage else null,
signature = Hex.encodeHexString(msg.signature.signature.toByteArray()),
nonce = msg.signature.nonce
)
}

View File

@@ -116,7 +116,7 @@ abstract class BaseHandler(
// If Proxy is configured to preserve original order it means that a client expect responses at exact same position
// as requests even if a request completely failed for a some reason. It's very unlikely situation, but still possible
// At this case, if we found a gap in responses, we put a default response with an error
?: NativeCall.CallResult(id, null, NativeCall.CallError(id, "No response", null))
?: NativeCall.CallResult(id, null, null, NativeCall.CallError(id, "No response", null), null)
}
}
.flatMapMany {

View File

@@ -180,7 +180,7 @@ class WebsocketHandler(
}
Mono.just(response)
.map { Global.objectMapper.writeValueAsString(it) }
.doOnNext { eventHandler.onResponse(NativeCall.CallResult.ok(0, it.toByteArray())) }
.doOnNext { eventHandler.onResponse(NativeCall.CallResult.ok(0, null, it.toByteArray(), null)) }
.doFinally { eventHandler.close() }
} else {
val eventHandler: AccessHandlerHttp.RequestHandler = eventHandlerFactory.call()

View File

@@ -20,12 +20,14 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
open class AlwaysQuorum : CallQuorum {
private var resolved = false
private var result: ByteArray? = null
private var rpcError: JsonRpcError? = null
private var sig: ResponseSigner.Signature? = null
override fun init(head: Head) {
}
@@ -38,14 +40,20 @@ open class AlwaysQuorum : CallQuorum {
return rpcError != null
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
override fun getSignature(): ResponseSigner.Signature? {
return sig
}
override fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean {
result = response
resolved = true
sig = signature
return true
}
override fun record(error: JsonRpcException, upstream: Upstream) {
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
this.rpcError = error.error
sig = signature
}
override fun getResult(): ByteArray? {

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
open class BroadcastQuorum(
val quorum: Int = 3
@@ -26,6 +27,7 @@ open class BroadcastQuorum(
private var result: ByteArray? = null
private var txid: String? = null
private var calls = 0
private var sig: ResponseSigner.Signature? = null
override fun init(head: Head) {
}
@@ -42,19 +44,25 @@ open class BroadcastQuorum(
return result
}
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
override fun getSignature(): ResponseSigner.Signature? {
return sig
}
override fun recordValue(response: ByteArray, responseValue: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
calls++
if (txid == null && responseValue != null) {
txid = responseValue
sig = signature
result = response
}
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
override fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
calls++
if (result == null) {
result = response
sig = signature
}
}

View File

@@ -20,9 +20,7 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import reactor.util.function.Tuple2
import java.util.function.BiFunction
import java.util.function.Predicate
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
interface CallQuorum {
@@ -31,23 +29,9 @@ interface CallQuorum {
fun isResolved(): Boolean
fun isFailed(): Boolean
fun record(response: ByteArray, upstream: Upstream): Boolean
fun record(error: JsonRpcException, upstream: Upstream)
fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean
fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream)
fun getSignature(): ResponseSigner.Signature?
fun getResult(): ByteArray?
fun getError(): JsonRpcError?
companion object {
fun untilResolved(cq: CallQuorum): Predicate<Any> {
return Predicate { _ ->
!cq.isResolved()
}
}
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
a.record(b.t1, b.t2)
return@BiFunction a
}
}
}
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
open class NonEmptyQuorum(
val maxTries: Int = 3
@@ -25,7 +26,7 @@ open class NonEmptyQuorum(
private var result: ByteArray? = null
private var tries: Int = 0
private var sig: ResponseSigner.Signature? = null
override fun init(head: Head) {
}
@@ -37,10 +38,15 @@ open class NonEmptyQuorum(
return tries >= maxTries
}
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
override fun getSignature(): ResponseSigner.Signature? {
return sig
}
override fun recordValue(response: ByteArray, responseValue: Any?, signature: ResponseSigner.Signature?, upstream: Upstream) {
tries++
if (responseValue != null) {
result = response
sig = signature
}
}
@@ -48,7 +54,7 @@ open class NonEmptyQuorum(
return result
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
override fun recordError(response: ByteArray?, errorMessage: String?, sig: ResponseSigner.Signature?, upstream: Upstream) {
tries++
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.etherjar.hex.HexQuantity
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -31,6 +32,7 @@ open class NonceQuorum(
private var result: ByteArray? = null
private var receivedTimes = 0
private var errors = 0
private var sig: ResponseSigner.Signature? = null
override fun init(head: Head) {
}
@@ -44,8 +46,11 @@ open class NonceQuorum(
override fun isFailed(): Boolean {
return errors >= tries
}
override fun getSignature(): ResponseSigner.Signature? {
return sig
}
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
override fun recordValue(response: ByteArray, responseValue: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
val value = responseValue?.let { str ->
HexQuantity.from(str).value.toLong()
}
@@ -54,8 +59,10 @@ open class NonceQuorum(
if (value != null && value > resultValue) {
resultValue = value
result = response
sig = signature
} else if (result == null) {
result = response
sig = signature
}
}
}
@@ -64,7 +71,7 @@ open class NonceQuorum(
return result
}
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
override fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
errors++
}

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import java.util.concurrent.atomic.AtomicReference
/**
@@ -32,6 +33,7 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
private val result: AtomicReference<ByteArray> = AtomicReference()
private val failed = AtomicReference(false)
private var rpcError: JsonRpcError? = null
private var sig: ResponseSigner.Signature? = null
override fun init(head: Head) {
}
@@ -44,16 +46,17 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
return failed.get()
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
override fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean {
val lagging = upstream.getLag() > maxLag
if (!lagging) {
result.set(response)
sig = signature
return true
}
return false
}
override fun record(error: JsonRpcException, upstream: Upstream) {
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
this.rpcError = error.error
val lagging = upstream.getLag() > maxLag
if (!lagging && result.get() == null) {
@@ -61,6 +64,9 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
}
}
override fun getSignature(): ResponseSigner.Signature? {
return sig
}
override fun getResult(): ByteArray {
return result.get()
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
// creates instance of a Quorum based reader
interface QuorumReaderFactory {
@@ -28,11 +29,11 @@ interface QuorumReaderFactory {
}
}
fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result>
fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?): Reader<JsonRpcRequest, QuorumRpcReader.Result>
class Default : QuorumReaderFactory {
override fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
return QuorumRpcReader(apis, quorum)
override fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
return QuorumRpcReader(apis, quorum, signer)
}
}
}

View File

@@ -22,12 +22,15 @@ 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
import io.emeraldpay.etherjar.rpc.RpcException
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.util.function.Tuple2
import reactor.util.function.Tuple3
import reactor.util.function.Tuples
import java.util.Optional
import java.util.function.BiFunction
import java.util.function.Function
@@ -36,13 +39,16 @@ import java.util.function.Function
*/
class QuorumRpcReader(
private val apiControl: ApiSource,
private val quorum: CallQuorum
private val quorum: CallQuorum,
private val signer: ResponseSigner?,
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
companion object {
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
}
constructor(apiControl: ApiSource, quorum: CallQuorum) : this(apiControl, quorum, null)
override fun read(key: JsonRpcRequest): Mono<Result> {
// needs at least one response, so start a request
apiControl.request(1)
@@ -83,8 +89,8 @@ class QuorumRpcReader(
}
fun execute(key: JsonRpcRequest, retrySpec: reactor.util.retry.Retry): Function<Flux<Upstream>, Mono<CallQuorum>> {
val quorumReduce = BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { res, a ->
if (res.record(a.t1, a.t2)) {
val quorumReduce = BiFunction<CallQuorum, Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>, CallQuorum> { res, a ->
if (res.record(a.t1, a.t2.orElse(null), a.t3)) {
apiControl.resolve()
} else {
// quorum needs more responses, so ask api controller to make another
@@ -112,19 +118,43 @@ class QuorumRpcReader(
.filter { it.isResolved() } // return nothing if not resolved
.map {
// TODO find actual quorum number
QuorumRpcReader.Result(it.getResult()!!, 1)
QuorumRpcReader.Result(it.getResult()!!, it.getSignature(), 1)
}
.switchIfEmpty(defaultResult)
}
}
fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple2<ByteArray, Upstream>> {
fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>> {
return api.getApi()
.read(key)
.flatMap { response ->
response.requireResult()
.onErrorResume { err ->
// on error notify quorum, it may use error message or other details
.transform(withSignature(api, key, response))
.transform(withErrorResume(api, key))
}
.map { Tuples.of(it.t1, it.t2, api) }
}
fun withSignature(api: Upstream, key: JsonRpcRequest, response: JsonRpcResponse): Function<Mono<ByteArray>, Mono<Tuple2<ByteArray, Optional<ResponseSigner.Signature>>>> {
return Function { src ->
src.map {
val signature = response.providedSignature
?: if (key.nonce != null) {
signer?.sign(key.nonce, response.getResult(), api)
} else {
null
}
Tuples.of(it, Optional.ofNullable(signature))
}
}
}
fun <T> withErrorResume(api: Upstream, key: JsonRpcRequest): Function<Mono<T>, Mono<T>> {
return Function { src ->
src.onErrorResume { err ->
// when the call failed with an error we want to notify the quorum because
// it may use the error message or other details
//
val cleanErr: JsonRpcException = when (err) {
is RpcException -> JsonRpcException.from(err)
is JsonRpcException -> err
@@ -133,7 +163,7 @@ class QuorumRpcReader(
JsonRpcError(-32603, "Unhandled internal error: ${err.javaClass}")
)
}
quorum.record(cleanErr, api)
quorum.record(cleanErr, null, api)
// if it's failed after that, then we don't need more calls, stop api source
if (quorum.isFailed()) {
apiControl.resolve()
@@ -143,7 +173,6 @@ class QuorumRpcReader(
Mono.empty()
}
}
.map { Tuples.of(it, api) }
}
fun setupDefaultResult(key: JsonRpcRequest): Mono<Result> {
@@ -162,6 +191,7 @@ class QuorumRpcReader(
class Result(
val value: ByteArray,
val signature: ResponseSigner.Signature?,
val quorum: Int
)
}

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.etherjar.rpc.RpcException
import org.slf4j.LoggerFactory
@@ -34,26 +35,26 @@ abstract class ValueAwareQuorum<T>(
return Global.objectMapper.readValue(response.inputStream(), clazz)
}
override fun record(response: ByteArray, upstream: Upstream): Boolean {
override fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean {
try {
val value = extractValue(response, clazz)
recordValue(response, value, upstream)
recordValue(response, value, signature, upstream)
} catch (e: RpcException) {
recordError(response, e.rpcMessage, upstream)
recordError(response, e.rpcMessage, signature, upstream)
} catch (e: Exception) {
recordError(response, e.message, upstream)
recordError(response, e.message, signature, upstream)
}
return isResolved()
}
override fun record(error: JsonRpcException, upstream: Upstream) {
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
this.rpcError = error.error
recordError(null, error.error.message, upstream)
recordError(null, error.error.message, signature, upstream)
}
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
abstract fun recordValue(response: ByteArray, responseValue: T?, signature: ResponseSigner.Signature?, upstream: Upstream)
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
abstract fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream)
override fun getError(): JsonRpcError? {
return rpcError

View File

@@ -44,7 +44,7 @@ class BlockchainRpc(
@Autowired private val trackAddress: List<TrackAddress>,
@Autowired private val describe: Describe,
@Autowired private val subscribeStatus: SubscribeStatus,
@Autowired private val estimateFee: EstimateFee
@Autowired private val estimateFee: EstimateFee,
) : ReactorBlockchainGrpc.BlockchainImplBase() {
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)

View File

@@ -34,6 +34,7 @@ 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
import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.grpc.BlockchainType
@@ -49,7 +50,8 @@ import java.util.EnumMap
@Service
open class NativeCall(
@Autowired private val multistreamHolder: MultistreamHolder
@Autowired private val multistreamHolder: MultistreamHolder,
@Autowired private val signer: ResponseSigner,
) {
private val log = LoggerFactory.getLogger(NativeCall::class.java)
@@ -85,7 +87,7 @@ open class NativeCall(
} else {
val error = it.getError()
Mono.just(
CallResult(error.id, null, error)
CallResult(error.id, 0, null, error, null)
)
}
}
@@ -107,10 +109,21 @@ open class NativeCall(
} else {
result.payload = ByteString.copyFrom(it.result)
}
if (it.nonce != null && it.signature != null) {
result.signature = buildSignature(it.nonce, it.signature)
}
return result.build()
}
fun buildSignature(nonce: Long, signature: ResponseSigner.Signature): BlockchainOuterClass.NativeCallReplySignature {
val msg = BlockchainOuterClass.NativeCallReplySignature.newBuilder()
msg.signature = ByteString.copyFrom(signature.value)
msg.keyId = signature.keyId
msg.upstreamId = signature.upstreamId
msg.nonce = nonce
return msg.build()
}
fun processException(it: Throwable?): Mono<BlockchainOuterClass.NativeCallReplyItem> {
val id: Int = if (it != null && it is CallFailure) {
it.id
@@ -171,7 +184,6 @@ open class NativeCall(
)
)
}
// for ethereum the actual block needed for the call may be specified in the call parameters
val callSpecificMatcher: Mono<Selector.Matcher> =
if (BlockchainType.from(upstream.chain) == BlockchainType.ETHEREUM) {
@@ -179,7 +191,6 @@ open class NativeCall(
} else {
null
} ?: Mono.empty()
return callSpecificMatcher.defaultIfEmpty(Selector.empty).map { csm ->
val matcher = Selector.Builder()
.withMatcher(csm)
@@ -196,27 +207,27 @@ open class NativeCall(
val heightMatcher = Selector.HeightMatcher(minHeight)
matcher.withMatcher(heightMatcher)
}
ValidCallContext(requestItem.id, upstream, matcher.build(), callQuorum, RawCallDetails(method, params))
val nonce = requestItem.nonce.let { if (it == 0L) null else it }
ValidCallContext(requestItem.id, nonce, upstream, matcher.build(), callQuorum, RawCallDetails(method, params))
}
}
fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> {
return ctx.upstream.getRoutedApi(ctx.matcher)
.flatMap { api ->
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce))
.flatMap(JsonRpcResponse::requireResult)
.map {
CallResult.ok(ctx.id, it)
CallResult.ok(ctx.id, ctx.nonce, it, null)
}
}.switchIfEmpty(
Mono.just(ctx).flatMap(this::executeOnRemote)
)
.onErrorResume {
if (it is CallFailure) {
Mono.just(CallResult.fail(it.id, it.reason))
Mono.just(CallResult.fail(it.id, ctx.nonce, it.reason))
} else {
Mono.just(CallResult.fail(ctx.id, it))
Mono.just(CallResult.fail(ctx.id, ctx.nonce, it))
}
}
}
@@ -226,22 +237,22 @@ open class NativeCall(
if (!ctx.upstream.getMethods().isCallable(ctx.payload.method)) {
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
}
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum)
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum, signer)
return reader
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce))
.map {
CallResult(ctx.id, it.value, null)
CallResult(ctx.id, ctx.nonce, it.value, null, it.signature)
}
.onErrorResume { t ->
val failure = when (t) {
is CallFailure -> CallResult.fail(t.id, t.reason)
is JsonRpcException -> CallResult.fail(ctx.id, t.error.code, t.error.message)
else -> CallResult.fail(ctx.id, 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.just(CallResult.fail(ctx.id, 1, "No response or no available upstream for ${ctx.payload.method}"))
Mono.just(CallResult.fail(ctx.id, ctx.nonce, 1, "No response or no available upstream for ${ctx.payload.method}"))
)
}
@@ -262,6 +273,7 @@ open class NativeCall(
open class ValidCallContext<T>(
val id: Int,
val nonce: Long?,
val upstream: Multistream,
val matcher: Selector.Matcher,
val callQuorum: CallQuorum,
@@ -280,7 +292,7 @@ open class NativeCall(
}
fun <X> withPayload(payload: X): ValidCallContext<X> {
return ValidCallContext(id, upstream, matcher, callQuorum, payload)
return ValidCallContext(id, nonce, upstream, matcher, callQuorum, payload)
}
fun getApis(): ApiSource {
@@ -322,18 +334,18 @@ open class NativeCall(
}
}
open class CallResult(val id: Int, val result: ByteArray?, val error: CallError?) {
open class CallResult(val id: Int, val nonce: Long?, val result: ByteArray?, val error: CallError?, val signature: ResponseSigner.Signature?) {
companion object {
fun ok(id: Int, result: ByteArray): CallResult {
return CallResult(id, result, null)
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?): CallResult {
return CallResult(id, nonce, result, null, signature)
}
fun fail(id: Int, errorCore: Int, errorMessage: String): CallResult {
return CallResult(id, null, CallError(errorCore, errorMessage, null))
fun fail(id: Int, nonce: Long?, errorCore: Int, errorMessage: String): CallResult {
return CallResult(id, nonce, null, CallError(errorCore, errorMessage, null), null)
}
fun fail(id: Int, error: Throwable): CallResult {
return CallResult(id, null, CallError.from(error))
fun fail(id: Int, nonce: Long?, error: Throwable): CallResult {
return CallResult(id, nonce, null, CallError.from(error), null)
}
}

View File

@@ -15,7 +15,6 @@
*/
package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.SilentException
@@ -155,7 +154,6 @@ class TrackBitcoinTx(
Common.BlockInfo.newBuilder()
.setBlockId(tx.blockHash!!.substring(2))
.setTimestamp(tx.blockTime!!.toEpochMilli())
.setWeight(ByteString.copyFrom(tx.blockTotalDifficulty!!.toByteArray()))
.setHeight(tx.height!!)
)
}

View File

@@ -16,7 +16,6 @@
*/
package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.SilentException
@@ -258,7 +257,6 @@ class TrackEthereumTx(
Common.BlockInfo.newBuilder()
.setBlockId(tx.status.blockHash!!.toHex().substring(2))
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
.setWeight(ByteString.copyFrom(tx.status.blockTotalDifficulty!!.toByteArray()))
.setHeight(tx.status.height!!)
)
}

View File

@@ -64,6 +64,12 @@ abstract class DefaultUpstream(
.multicast()
.directBestEffort<UpstreamAvailability>()
init {
if (id.length < 3 || !id.matches(Regex("[a-zA-Z][a-zA-Z0-9_-]+[a-zA-Z0-9]"))) {
throw IllegalArgumentException("Invalid upstream id: $id")
}
}
override fun isAvailable(): Boolean {
return getStatus() == UpstreamAvailability.OK
}

View File

@@ -132,7 +132,8 @@ class EthereumDirectReader(
*/
private fun readWithQuorum(request: JsonRpcRequest): Mono<ByteArray> {
return quorumReaderFactory
.create(up.getApiSource(Selector.empty), callMethodsFactory.create().getQuorumFor(request.method))
// we do not use Signer for internal requests because it doesn't make much sense
.create(up.getApiSource(Selector.empty), callMethodsFactory.create().getQuorumFor(request.method), null)
.read(request)
.map { it.value }
}

View File

@@ -56,9 +56,14 @@ class LocalCallRouter(
return Mono.just(methods.executeHardcoded(key.method))
.map { JsonRpcResponse(it, null) }
}
if (!methods.isCallable(key.method)) {
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
}
if (key.nonce != null) {
// we do not want to serve any requests (except hardcoded) that have nonces from cache
return Mono.empty()
}
val common = commonRequests(key)
if (common != null) {
return common.map { JsonRpcResponse(it, null) }

View File

@@ -283,7 +283,7 @@ class WsConnection(
fun onRpc(msg: ResponseWSParser.WsResponse): Mono<Void> {
return if (msg.id.isNumber()) {
val resp = JsonRpcResponse(
msg.value, msg.error, msg.id
msg.value, msg.error, msg.id, null
)
Mono.fromCallable {
val status = rpcReceive.tryEmitNext(resp)
@@ -377,7 +377,7 @@ class WsConnection(
RpcResponseError.CODE_INTERNAL_ERROR,
"Response not received from WebSocket"
),
JsonRpcResponse.Id.from(originalId)
JsonRpcResponse.Id.from(originalId), null
)
return Flux.from(rpcReceive.asFlux())

View File

@@ -40,6 +40,7 @@ import org.springframework.context.Lifecycle
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.time.Instant
import java.util.Locale
import java.util.concurrent.TimeoutException
import java.util.function.Function
@@ -50,7 +51,7 @@ class BitcoinGrpcUpstream(
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val client: JsonRpcGrpcClient
) : BitcoinUpstream(
"$parentId/${chain.chainCode}",
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
chain,
UpstreamsConfig.Options.getDefaults(),
role

View File

@@ -43,6 +43,7 @@ import org.springframework.context.Lifecycle
import reactor.core.publisher.Mono
import java.math.BigInteger
import java.time.Instant
import java.util.Locale
import java.util.concurrent.TimeoutException
import java.util.function.Function
@@ -53,7 +54,7 @@ open class EthereumGrpcUpstream(
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val client: JsonRpcGrpcClient
) : EthereumUpstream(
"$parentId/${chain.chainCode}",
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
UpstreamsConfig.Options.getDefaults(),
role,
null, null

View File

@@ -17,10 +17,12 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallReplySignature
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.grpc.Chain
@@ -60,13 +62,14 @@ class JsonRpcGrpcClient(
}
}
BlockchainOuterClass.NativeCallItem.newBuilder()
val reqItem = BlockchainOuterClass.NativeCallItem.newBuilder()
.setId(1)
.setMethod(key.method)
.setPayload(ByteString.copyFrom(Global.objectMapper.writeValueAsBytes(key.params)))
.build().let {
req.addItems(it)
if (key.nonce != null) {
reqItem.nonce = key.nonce
}
req.addItems(reqItem.build())
return Mono.just(key)
.doOnNext {
@@ -77,7 +80,12 @@ class JsonRpcGrpcClient(
.flatMap { resp ->
if (resp.succeed) {
val bytes = resp.payload.toByteArray()
Mono.just(JsonRpcResponse(bytes, null))
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(
@@ -96,5 +104,16 @@ class JsonRpcGrpcClient(
}
}
}
fun extractSignature(resp: NativeCallReplySignature?): ResponseSigner.Signature? {
if (resp == null || resp.signature == null || resp.signature.isEmpty || resp.upstreamId == null || resp.upstreamId.isEmpty()) {
return null
}
return ResponseSigner.Signature(
resp.signature.toByteArray(),
resp.upstreamId,
resp.keyId
)
}
}
}

View File

@@ -24,10 +24,11 @@ import io.emeraldpay.dshackle.Global
data class JsonRpcRequest(
val method: String,
val params: List<Any?>,
val id: Int
val id: Int,
val nonce: Long?
) {
constructor(method: String, params: List<Any?>) : this(method, params, 1)
@JvmOverloads constructor(method: String, params: List<Any?>, nonce: Long? = null) : this(method, params, 1, nonce)
fun toJson(): ByteArray {
val json = mapOf(
@@ -62,7 +63,7 @@ data class JsonRpcRequest(
throw IllegalStateException("Unsupported param type: ${it.asToken()}")
}
}
return JsonRpcRequest(method, params, id)
return JsonRpcRequest(method, params, id, null)
}
}
}

View File

@@ -18,12 +18,18 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import reactor.core.publisher.Mono
class JsonRpcResponse(
private val result: ByteArray?,
val error: JsonRpcError?,
val id: Id
val id: Id,
/**
* When making a request through Dshackle protocol a remote may provide its signature with the response, which we keep here
*/
val providedSignature: ResponseSigner.Signature? = null
) {
constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, NumberId(0))
@@ -107,7 +113,11 @@ class JsonRpcResponse(
}
fun copyWithId(id: Id): JsonRpcResponse {
return JsonRpcResponse(result, error, id)
return JsonRpcResponse(result, error, id, providedSignature)
}
fun copyWithSignature(signature: ResponseSigner.Signature): JsonRpcResponse {
return JsonRpcResponse(result, error, id, signature)
}
override fun equals(other: Any?): Boolean {

View File

@@ -25,11 +25,11 @@ open class ResponseRpcParser : ResponseParser<JsonRpcResponse>() {
override fun build(state: Preparsed): JsonRpcResponse {
if (state.error != null) {
return JsonRpcResponse(null, state.error, state.id ?: JsonRpcResponse.Id.from(-1))
return JsonRpcResponse(null, state.error, state.id ?: JsonRpcResponse.Id.from(-1), null)
}
if (state.nullResult) {
return JsonRpcResponse("null".toByteArray(), null, state.id ?: JsonRpcResponse.Id.from(-1))
return JsonRpcResponse("null".toByteArray(), null, state.id ?: JsonRpcResponse.Id.from(-1), null)
}
return JsonRpcResponse(state.result, null, state.id ?: JsonRpcResponse.Id.from(-1))
return JsonRpcResponse(state.result, null, state.id ?: JsonRpcResponse.Id.from(-1), null)
}
}

View File

@@ -0,0 +1,9 @@
package io.emeraldpay.dshackle.upstream.signature
import io.emeraldpay.dshackle.upstream.Upstream
class NoSigner : ResponseSigner {
override fun sign(nonce: Long, message: ByteArray, source: Upstream): ResponseSigner.Signature? {
return null
}
}

View File

@@ -0,0 +1,32 @@
package io.emeraldpay.dshackle.upstream.signature
import io.emeraldpay.dshackle.upstream.Upstream
interface ResponseSigner {
fun sign(nonce: Long, message: ByteArray, source: Upstream): Signature?
data class Signature(
val value: ByteArray,
val upstreamId: String,
val keyId: Long,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Signature) return false
if (!value.contentEquals(other.value)) return false
if (upstreamId != other.upstreamId) return false
if (keyId != other.keyId) return false
return true
}
override fun hashCode(): Int {
var result = value.contentHashCode()
result = 31 * result + upstreamId.hashCode()
result = 31 * result + keyId.hashCode()
return result
}
}
}

View File

@@ -0,0 +1,88 @@
package io.emeraldpay.dshackle.upstream.signature
import io.emeraldpay.dshackle.config.SignatureConfig
import org.apache.commons.codec.binary.Hex
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.spec.ECPublicKeySpec
import org.bouncycastle.math.ec.ECPoint
import org.bouncycastle.util.io.pem.PemObject
import org.bouncycastle.util.io.pem.PemReader
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.FactoryBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import java.nio.ByteBuffer
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PublicKey
import java.security.interfaces.ECPrivateKey
import java.security.spec.PKCS8EncodedKeySpec
@Repository
open class ResponseSignerFactory(
@Autowired private val config: SignatureConfig
) : FactoryBean<ResponseSigner> {
companion object {
private val log = LoggerFactory.getLogger(ResponseSignerFactory::class.java)
}
fun readKey(algorithm: SignatureConfig.Algorithm, keyPath: String): Pair<ECPrivateKey, Long> {
val reader = PemReader(Files.newBufferedReader(Path.of(keyPath)))
return readKey(algorithm, reader.readPemObject())
}
private fun readKey(algorithm: SignatureConfig.Algorithm, pem: PemObject): Pair<ECPrivateKey, Long> {
val keyFactory = KeyFactory.getInstance("EC")
val key = when (algorithm) {
SignatureConfig.Algorithm.SECP256K1 -> {
val keySpec = PKCS8EncodedKeySpec(pem.content)
keyFactory.generatePrivate(keySpec)
}
}
if (key !is ECPrivateKey) {
throw IllegalStateException("Only ECDSA SECP256K1 keys are allowed")
}
if (key.params.toString() != "secp256k1 (1.3.132.0.10)") {
throw IllegalStateException("Only SECP256K1 are allowed for signing a response")
}
val publicKey = extractPublicKey(keyFactory, key)
val id = getPublicKeyId(publicKey)
return Pair(key, id)
}
fun extractPublicKey(keyFactory: KeyFactory, privateKey: ECPrivateKey): PublicKey {
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
val q: ECPoint = ecSpec.g.multiply(privateKey.s)
return keyFactory.generatePublic(ECPublicKeySpec(q, ecSpec))
}
private fun getPublicKeyId(publicKey: PublicKey): Long {
val digest = MessageDigest.getInstance("SHA-256")
val fullId = digest.digest(publicKey.encoded)
log.info("Using key to sign responses: ${Hex.encodeHexString(fullId).substring(0..15)}")
return ByteBuffer.wrap(fullId).asLongBuffer().get()
}
override fun getObject(): ResponseSigner {
if (!config.enabled) {
return NoSigner()
}
if (config.privateKey == null) {
log.warn("Private Key for response signature is not set")
return NoSigner()
}
val key = readKey(config.algorithm, config.privateKey!!)
return Secp256KSigner(key.first, key.second)
}
override fun getObjectType(): Class<*>? {
return ResponseSigner::class.java
}
}

View File

@@ -0,0 +1,60 @@
package io.emeraldpay.dshackle.upstream.signature
import io.emeraldpay.dshackle.upstream.Upstream
import org.apache.commons.codec.binary.Hex
import java.security.MessageDigest
import java.security.Signature
import java.security.interfaces.ECPrivateKey
class Secp256KSigner(
private val privateKey: ECPrivateKey,
val keyId: Long,
) : ResponseSigner {
companion object {
const val SIGN_SCHEME = "SHA256withECDSA"
const val MSG_PREFIX = "DSHACKLESIG"
const val MSG_SEPARATOR = '/'
}
override fun sign(nonce: Long, message: ByteArray, source: Upstream): ResponseSigner.Signature {
val sig = Signature.getInstance(SIGN_SCHEME)
sig.initSign(privateKey)
val wrapped = wrapMessage(nonce, message, source)
sig.update(wrapped.toByteArray())
val value = sig.sign()
return ResponseSigner.Signature(
value, source.getId(), keyId
)
}
/**
* To avoid various attacks, such as various kinds of padding and message alternating attacks,
* we (1) tag the original message to specify the source, (2) ensure no parts of the message can affect each other
* and (3) ensure the message cannot break the wrapping.
*
* We're doing that by converting the message as `"DSHACKLESIG/" || str(nonce) || "/" || hex(sha256(msg))`
*
* I.e.:
* - three elements in the wrapped message
* - separated by "/" which is not a part of any element
* - first element is DSHACKLESIG tag
* - second is the nonce value encode as decimal string
* - third is SHA256 hash of the original message encoded as hex string
*/
fun wrapMessage(nonce: Long, message: ByteArray, source: Upstream): String {
val sha256 = MessageDigest.getInstance("SHA-256")
// we create it with max capacity that we expect for the result, which is total lengths of its parts
val formatterMsg = StringBuilder(11 + 1 + 18 + 1 + 64 + 1 + 64)
formatterMsg.append(MSG_PREFIX)
.append(MSG_SEPARATOR)
.append(nonce.toString())
.append(MSG_SEPARATOR)
// We expect that the id is short enough (less than 64 symbols) and also it doesn't contain the `/` symbol
// which is verified in UpstreamConfigReader and DefaultUpstream constructor
.append(source.getId())
.append(MSG_SEPARATOR)
.append(Hex.encodeHexString(sha256.digest(message)))
return formatterMsg.toString()
}
}

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-----

View File

@@ -31,6 +31,12 @@ cache:
redis:
enabled: false
signature:
enabled: true
algorithm: ECDSA
signatureScheme: SHA256withECDSA
privateKey: "./test_key"
proxy:
port: 18080
tls:

View File

@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQglWZBwGvH/I/TqQb3uPGq
d/6MB2tgFXUfQCYj5RmaXV2hRANCAATfN610x3JM7+xMUIt46sjmaiJm3ZGN9RV1
q+GiuCp36DECozTVx/lDhBnwg2d71HKRxCxsthdB8NclsYybn6B2
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE3zetdMdyTO/sTFCLeOrI5moiZt2RjfUV
davhorgqd+gxAqM01cf5Q4QZ8INne9RykcQsbLYXQfDXJbGMm5+gdg==
-----END PUBLIC KEY-----

View File

@@ -7,6 +7,7 @@ plugins {
repositories {
mavenLocal()
mavenCentral()
maven { url "https://maven.emrld.io" }
}
dependencies {
@@ -14,12 +15,25 @@ dependencies {
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.11.1") {
exclude group: 'com.salesforce.servicelibs', module: 'reactor-grpc'
}
testImplementation "org.spockframework:spock-core:2.0-M3-groovy-3.0"
}
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute module("io.emeraldpay:emerald-api") using project(":api") because "we work with the unreleased development version"
}
}
test {
systemProperty "trialMode", project.getProperty("dshackleTrialMode")
systemProperty "signatureKey", project.getProperty("signatureKey")
useJUnitPlatform()
testLogging {
events "PASSED", "FAILED"

View File

@@ -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)
}

View File

@@ -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))
}
}
}

View File

@@ -12,7 +12,7 @@ import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.impl.client.HttpClients
class ProxyClient {
class ProxyClient implements Client {
private int sequence = 0;

View File

@@ -1,13 +1,28 @@
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 {
def client = ProxyClient.forPrefix("eth")
@Shared client_proto = ProtoClient.basic()
@Shared client_proxy = ProxyClient.forPrefix("eth")
@Shared clients = [client_proto, client_proxy]
def "get height"() {
when:
@@ -15,6 +30,8 @@ class StandardCallsSpec extends Specification {
then:
act.result == "0x100001"
act.error == null
where:
client << clients
}
def "get block"() {
@@ -31,6 +48,8 @@ class StandardCallsSpec extends Specification {
]
}
act.error == null
where:
client << clients
}
def "get non-existing block"() {
@@ -39,6 +58,8 @@ class StandardCallsSpec extends Specification {
then:
act.result == null
act.error == null
where:
client << clients
}
def "get tx"() {
@@ -50,6 +71,8 @@ class StandardCallsSpec extends Specification {
blockHash == "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e"
}
act.error == null
where:
client << clients
}
def "get non-existing tx"() {
@@ -58,6 +81,8 @@ class StandardCallsSpec extends Specification {
then:
act.result == null
act.error == null
where:
client << clients
}
def "get block with txes"() {
@@ -76,6 +101,8 @@ class StandardCallsSpec extends Specification {
}
}
act.error == null
where:
client << clients
}
def "returns original block json"() {
@@ -87,6 +114,8 @@ class StandardCallsSpec extends Specification {
testFoo == "bar"
}
act.error == null
where:
client << clients
}
def "returns original block json with tx"() {
@@ -98,5 +127,33 @@ class StandardCallsSpec extends Specification {
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())
sig.update(Bytes.concat("DSHACKLESIG".bytes, sep, Longs.toByteArray(10), sep, messageHash))
then:
(new String(act.payload.toByteArray())) == "\"0x100001\""
sig.verify(act.signature.sig.toByteArray())
}
def "check response signature without nonce"() {
when:
def act = client_proto.executeNative("eth_blockNumber", [], 0L)
then:
act.signature.sig.isEmpty()
}
}