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:
committed by
GitHub
parent
530811d272
commit
d1ad77a345
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,38 +118,61 @@ 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
|
||||
val cleanErr: JsonRpcException = when (err) {
|
||||
is RpcException -> JsonRpcException.from(err)
|
||||
is JsonRpcException -> err
|
||||
else -> JsonRpcException(
|
||||
JsonRpcResponse.NumberId(key.id),
|
||||
JsonRpcError(-32603, "Unhandled internal error: ${err.javaClass}")
|
||||
)
|
||||
}
|
||||
quorum.record(cleanErr, api)
|
||||
// if it's failed after that, then we don't need more calls, stop api source
|
||||
if (quorum.isFailed()) {
|
||||
apiControl.resolve()
|
||||
} else {
|
||||
apiControl.request(1)
|
||||
}
|
||||
Mono.empty()
|
||||
}
|
||||
.transform(withSignature(api, key, response))
|
||||
.transform(withErrorResume(api, key))
|
||||
}
|
||||
.map { Tuples.of(it, api) }
|
||||
.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
|
||||
else -> JsonRpcException(
|
||||
JsonRpcResponse.NumberId(key.id),
|
||||
JsonRpcError(-32603, "Unhandled internal error: ${err.javaClass}")
|
||||
)
|
||||
}
|
||||
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()
|
||||
} else {
|
||||
apiControl.request(1)
|
||||
}
|
||||
Mono.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setupDefaultResult(key: JsonRpcRequest): Mono<Result> {
|
||||
@@ -162,6 +191,7 @@ class QuorumRpcReader(
|
||||
|
||||
class Result(
|
||||
val value: ByteArray,
|
||||
val signature: ResponseSigner.Signature?,
|
||||
val quorum: Int
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!!)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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!!)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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", []))
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
27
src/test/resources/tls-local/127.0.0.1.invalid.key
Normal file
27
src/test/resources/tls-local/127.0.0.1.invalid.key
Normal 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-----
|
||||
@@ -31,6 +31,12 @@ cache:
|
||||
redis:
|
||||
enabled: false
|
||||
|
||||
signature:
|
||||
enabled: true
|
||||
algorithm: ECDSA
|
||||
signatureScheme: SHA256withECDSA
|
||||
privateKey: "./test_key"
|
||||
|
||||
proxy:
|
||||
port: 18080
|
||||
tls:
|
||||
|
||||
5
testing/dshackle/test_key
Normal file
5
testing/dshackle/test_key
Normal file
@@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQglWZBwGvH/I/TqQb3uPGq
|
||||
d/6MB2tgFXUfQCYj5RmaXV2hRANCAATfN610x3JM7+xMUIt46sjmaiJm3ZGN9RV1
|
||||
q+GiuCp36DECozTVx/lDhBnwg2d71HKRxCxsthdB8NclsYybn6B2
|
||||
-----END PRIVATE KEY-----
|
||||
4
testing/dshackle/test_key.pub
Normal file
4
testing/dshackle/test_key.pub
Normal file
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE3zetdMdyTO/sTFCLeOrI5moiZt2RjfUV
|
||||
davhorgqd+gxAqM01cf5Q4QZ8INne9RykcQsbLYXQfDXJbGMm5+gdg==
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.emeraldpay.dshackle.testing.trial
|
||||
|
||||
interface Client {
|
||||
Map<String, Object> execute(String method, List<Object> params)
|
||||
Map<String, Object> execute(Object id, String method, List<Object> params)
|
||||
}
|
||||
@@ -0,0 +1,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user