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;
|
uint32 id = 1;
|
||||||
string method = 3;
|
string method = 3;
|
||||||
bytes payload = 4;
|
bytes payload = 4;
|
||||||
|
uint64 nonce = 5;
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -70,6 +71,14 @@ message NativeCallReplyItem {
|
|||||||
bool succeed = 2;
|
bool succeed = 2;
|
||||||
bytes payload = 3;
|
bytes payload = 3;
|
||||||
bytes error = 4;
|
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)
|
- or `error` if request failed (`succeed` is false)
|
||||||
|
|
||||||
NOTE: Reply Items comes right after their execution on an upstream, therefore streaming response.
|
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
|
=== Wrapped JSON RPC subscriptions
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ cache:
|
|||||||
db: 0
|
db: 0
|
||||||
password: I1y0dGKy01by
|
password: I1y0dGKy01by
|
||||||
|
|
||||||
|
signed-response:
|
||||||
|
enabled: true
|
||||||
|
algorithm: SECP256K1
|
||||||
|
private-key: /path/key.pem
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
port: 8080
|
port: 8080
|
||||||
@@ -199,6 +204,11 @@ See <<tokens>> section
|
|||||||
| Caching configuration.
|
| Caching configuration.
|
||||||
See <<cache>> section.
|
See <<cache>> section.
|
||||||
|
|
||||||
|
| `signed-response`
|
||||||
|
|
|
||||||
|
| Signed responses
|
||||||
|
See <<signed-response>> section.
|
||||||
|
|
||||||
| `cluster`
|
| `cluster`
|
||||||
|
|
|
|
||||||
| Setup connection to remote nodes.See <<cluster>> section
|
| 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]
|
||||||
== 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" }
|
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"
|
equals-verifier = "nl.jqno.equalsverifier:equalsverifier:3.3"
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,29 @@ message NativeCallItem {
|
|||||||
uint32 id = 1;
|
uint32 id = 1;
|
||||||
string method = 3;
|
string method = 3;
|
||||||
bytes payload = 4;
|
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 {
|
message NativeCallReplyItem {
|
||||||
@@ -41,6 +64,11 @@ message NativeCallReplyItem {
|
|||||||
bool succeed = 2;
|
bool succeed = 2;
|
||||||
bytes payload = 3;
|
bytes payload = 3;
|
||||||
string errorMessage = 4;
|
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 {
|
message NativeSubscribeRequest {
|
||||||
@@ -231,7 +259,7 @@ enum FeeEstimationMode {
|
|||||||
* Standard Ethereum Fee, supported by majority of forks and by Ethereum Mainnet before EIP-1559
|
* Standard Ethereum Fee, supported by majority of forks and by Ethereum Mainnet before EIP-1559
|
||||||
*/
|
*/
|
||||||
message EthereumStdFees {
|
message EthereumStdFees {
|
||||||
// Fee value in Wei
|
// Big Number encoded as string. Fee value in Wei
|
||||||
string fee = 1;
|
string fee = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,11 +267,11 @@ message EthereumStdFees {
|
|||||||
* Ethereum Fee for EIP-1559 compatible forks
|
* Ethereum Fee for EIP-1559 compatible forks
|
||||||
*/
|
*/
|
||||||
message EthereumExtFees {
|
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;
|
string expect = 1;
|
||||||
// Priority Fee in Wei
|
// Big Number encoded as string. Priority Fee in Wei
|
||||||
string priority = 2;
|
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;
|
string max = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,5 +280,5 @@ message EthereumExtFees {
|
|||||||
*/
|
*/
|
||||||
message BitcoinStdFees {
|
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.
|
// 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.MainConfig
|
||||||
import io.emeraldpay.dshackle.config.MainConfigReader
|
import io.emeraldpay.dshackle.config.MainConfigReader
|
||||||
import io.emeraldpay.dshackle.config.MonitoringConfig
|
import io.emeraldpay.dshackle.config.MonitoringConfig
|
||||||
|
import io.emeraldpay.dshackle.config.SignatureConfig
|
||||||
import io.emeraldpay.dshackle.config.TokensConfig
|
import io.emeraldpay.dshackle.config.TokensConfig
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
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.Scheduler
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.security.Security
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@@ -66,6 +69,8 @@ open class Config(
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConfigPath(): File {
|
fun getConfigPath(): File {
|
||||||
@@ -119,6 +124,11 @@ open class Config(
|
|||||||
return mainConfig.cache ?: CacheConfig()
|
return mainConfig.cache ?: CacheConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun signatureConfig(@Autowired mainConfig: MainConfig): SignatureConfig {
|
||||||
|
return mainConfig.signature ?: SignatureConfig()
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun tokensConfig(@Autowired mainConfig: MainConfig): TokensConfig {
|
open fun tokensConfig(@Autowired mainConfig: MainConfig): TokensConfig {
|
||||||
return mainConfig.tokens ?: TokensConfig(emptyList())
|
return mainConfig.tokens ?: TokensConfig(emptyList())
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ class MainConfig {
|
|||||||
var monitoring: MonitoringConfig = MonitoringConfig.default()
|
var monitoring: MonitoringConfig = MonitoringConfig.default()
|
||||||
var accessLogConfig: AccessLogConfig = AccessLogConfig.default()
|
var accessLogConfig: AccessLogConfig = AccessLogConfig.default()
|
||||||
var health: HealthConfig = HealthConfig.default()
|
var health: HealthConfig = HealthConfig.default()
|
||||||
|
var signature: SignatureConfig? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class MainConfigReader(
|
|||||||
private val monitoringConfigReader = MonitoringConfigReader()
|
private val monitoringConfigReader = MonitoringConfigReader()
|
||||||
private val accessLogReader = AccessLogReader()
|
private val accessLogReader = AccessLogReader()
|
||||||
private val healthConfigReader = HealthConfigReader()
|
private val healthConfigReader = HealthConfigReader()
|
||||||
|
private val signatureConfigReader = SignatureConfigReader(fileResolver)
|
||||||
|
|
||||||
fun read(input: InputStream): MainConfig? {
|
fun read(input: InputStream): MainConfig? {
|
||||||
val configNode = readNode(input)
|
val configNode = readNode(input)
|
||||||
@@ -75,6 +76,9 @@ class MainConfigReader(
|
|||||||
healthConfigReader.read(input).let {
|
healthConfigReader.read(input).let {
|
||||||
config.health = it
|
config.health = it
|
||||||
}
|
}
|
||||||
|
signatureConfigReader.read(input).let {
|
||||||
|
config.signature = it
|
||||||
|
}
|
||||||
return config
|
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 {
|
fun isValid(upstream: UpstreamsConfig.Upstream<*>): Boolean {
|
||||||
val id = upstream.id
|
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]"))) {
|
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")
|
log.warn("Invalid id: $id")
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ class Events {
|
|||||||
val payloadSizeBytes: Long,
|
val payloadSizeBytes: Long,
|
||||||
val nativeCall: NativeCallItemDetails,
|
val nativeCall: NativeCallItemDetails,
|
||||||
val responseBody: String? = null,
|
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)
|
) : ChainBase(blockchain, "NativeCall", id, channel)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
@@ -163,6 +165,7 @@ class Events {
|
|||||||
val method: String,
|
val method: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val payloadSizeBytes: Long,
|
val payloadSizeBytes: Long,
|
||||||
|
val nonce: Long,
|
||||||
val requestParams: String? = null
|
val requestParams: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.grpc.Attributes
|
|||||||
import io.grpc.Grpc
|
import io.grpc.Grpc
|
||||||
import io.grpc.Metadata
|
import io.grpc.Metadata
|
||||||
import io.netty.handler.codec.http.HttpHeaders
|
import io.netty.handler.codec.http.HttpHeaders
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import reactor.netty.http.server.HttpServerRequest
|
import reactor.netty.http.server.HttpServerRequest
|
||||||
@@ -312,6 +313,7 @@ class EventsBuilder {
|
|||||||
item.method,
|
item.method,
|
||||||
item.id,
|
item.id,
|
||||||
item.payload.size().toLong(),
|
item.payload.size().toLong(),
|
||||||
|
item.nonce,
|
||||||
if (accessLogConfig.includeMessages) {
|
if (accessLogConfig.includeMessages) {
|
||||||
if (item.payload != null && !item.payload.isEmpty && item.payload.isValidUtf8) item.payload.toStringUtf8() else ""
|
if (item.payload != null && !item.payload.isEmpty && item.payload.isValidUtf8) item.payload.toStringUtf8() else ""
|
||||||
} else null
|
} else null
|
||||||
@@ -336,6 +338,8 @@ class EventsBuilder {
|
|||||||
if (msg.payload != null && !msg.payload.isEmpty && msg.payload.isValidUtf8) msg.payload.toStringUtf8() else ""
|
if (msg.payload != null && !msg.payload.isEmpty && msg.payload.isValidUtf8) msg.payload.toStringUtf8() else ""
|
||||||
} else null,
|
} else null,
|
||||||
errorMessage = if (accessLogConfig.includeMessages) msg.errorMessage 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
|
// 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
|
// 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
|
// 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 {
|
.flatMapMany {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class WebsocketHandler(
|
|||||||
}
|
}
|
||||||
Mono.just(response)
|
Mono.just(response)
|
||||||
.map { Global.objectMapper.writeValueAsString(it) }
|
.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() }
|
.doFinally { eventHandler.close() }
|
||||||
} else {
|
} else {
|
||||||
val eventHandler: AccessHandlerHttp.RequestHandler = eventHandlerFactory.call()
|
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.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
|
|
||||||
open class AlwaysQuorum : CallQuorum {
|
open class AlwaysQuorum : CallQuorum {
|
||||||
|
|
||||||
private var resolved = false
|
private var resolved = false
|
||||||
private var result: ByteArray? = null
|
private var result: ByteArray? = null
|
||||||
private var rpcError: JsonRpcError? = null
|
private var rpcError: JsonRpcError? = null
|
||||||
|
private var sig: ResponseSigner.Signature? = null
|
||||||
|
|
||||||
override fun init(head: Head) {
|
override fun init(head: Head) {
|
||||||
}
|
}
|
||||||
@@ -38,14 +40,20 @@ open class AlwaysQuorum : CallQuorum {
|
|||||||
return rpcError != null
|
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
|
result = response
|
||||||
resolved = true
|
resolved = true
|
||||||
|
sig = signature
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: JsonRpcException, upstream: Upstream) {
|
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||||
this.rpcError = error.error
|
this.rpcError = error.error
|
||||||
|
sig = signature
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(): ByteArray? {
|
override fun getResult(): ByteArray? {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
|
|
||||||
open class BroadcastQuorum(
|
open class BroadcastQuorum(
|
||||||
val quorum: Int = 3
|
val quorum: Int = 3
|
||||||
@@ -26,6 +27,7 @@ open class BroadcastQuorum(
|
|||||||
private var result: ByteArray? = null
|
private var result: ByteArray? = null
|
||||||
private var txid: String? = null
|
private var txid: String? = null
|
||||||
private var calls = 0
|
private var calls = 0
|
||||||
|
private var sig: ResponseSigner.Signature? = null
|
||||||
|
|
||||||
override fun init(head: Head) {
|
override fun init(head: Head) {
|
||||||
}
|
}
|
||||||
@@ -42,19 +44,25 @@ open class BroadcastQuorum(
|
|||||||
return result
|
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++
|
calls++
|
||||||
if (txid == null && responseValue != null) {
|
if (txid == null && responseValue != null) {
|
||||||
txid = responseValue
|
txid = responseValue
|
||||||
|
sig = signature
|
||||||
result = response
|
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"
|
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
||||||
calls++
|
calls++
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = response
|
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.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
import reactor.util.function.Tuple2
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import java.util.function.BiFunction
|
|
||||||
import java.util.function.Predicate
|
|
||||||
|
|
||||||
interface CallQuorum {
|
interface CallQuorum {
|
||||||
|
|
||||||
@@ -31,23 +29,9 @@ interface CallQuorum {
|
|||||||
fun isResolved(): Boolean
|
fun isResolved(): Boolean
|
||||||
fun isFailed(): Boolean
|
fun isFailed(): Boolean
|
||||||
|
|
||||||
fun record(response: ByteArray, upstream: Upstream): Boolean
|
fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean
|
||||||
fun record(error: JsonRpcException, upstream: Upstream)
|
fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream)
|
||||||
|
fun getSignature(): ResponseSigner.Signature?
|
||||||
fun getResult(): ByteArray?
|
fun getResult(): ByteArray?
|
||||||
fun getError(): JsonRpcError?
|
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.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
|
|
||||||
open class NonEmptyQuorum(
|
open class NonEmptyQuorum(
|
||||||
val maxTries: Int = 3
|
val maxTries: Int = 3
|
||||||
@@ -25,7 +26,7 @@ open class NonEmptyQuorum(
|
|||||||
|
|
||||||
private var result: ByteArray? = null
|
private var result: ByteArray? = null
|
||||||
private var tries: Int = 0
|
private var tries: Int = 0
|
||||||
|
private var sig: ResponseSigner.Signature? = null
|
||||||
override fun init(head: Head) {
|
override fun init(head: Head) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +38,15 @@ open class NonEmptyQuorum(
|
|||||||
return tries >= maxTries
|
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++
|
tries++
|
||||||
if (responseValue != null) {
|
if (responseValue != null) {
|
||||||
result = response
|
result = response
|
||||||
|
sig = signature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +54,7 @@ open class NonEmptyQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, sig: ResponseSigner.Signature?, upstream: Upstream) {
|
||||||
tries++
|
tries++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.Head
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import io.emeraldpay.etherjar.hex.HexQuantity
|
import io.emeraldpay.etherjar.hex.HexQuantity
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
@@ -31,6 +32,7 @@ open class NonceQuorum(
|
|||||||
private var result: ByteArray? = null
|
private var result: ByteArray? = null
|
||||||
private var receivedTimes = 0
|
private var receivedTimes = 0
|
||||||
private var errors = 0
|
private var errors = 0
|
||||||
|
private var sig: ResponseSigner.Signature? = null
|
||||||
|
|
||||||
override fun init(head: Head) {
|
override fun init(head: Head) {
|
||||||
}
|
}
|
||||||
@@ -44,8 +46,11 @@ open class NonceQuorum(
|
|||||||
override fun isFailed(): Boolean {
|
override fun isFailed(): Boolean {
|
||||||
return errors >= tries
|
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 ->
|
val value = responseValue?.let { str ->
|
||||||
HexQuantity.from(str).value.toLong()
|
HexQuantity.from(str).value.toLong()
|
||||||
}
|
}
|
||||||
@@ -54,8 +59,10 @@ open class NonceQuorum(
|
|||||||
if (value != null && value > resultValue) {
|
if (value != null && value > resultValue) {
|
||||||
resultValue = value
|
resultValue = value
|
||||||
result = response
|
result = response
|
||||||
|
sig = signature
|
||||||
} else if (result == null) {
|
} else if (result == null) {
|
||||||
result = response
|
result = response
|
||||||
|
sig = signature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +71,7 @@ open class NonceQuorum(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
override fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||||
errors++
|
errors++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.upstream.Head
|
|||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
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 result: AtomicReference<ByteArray> = AtomicReference()
|
||||||
private val failed = AtomicReference(false)
|
private val failed = AtomicReference(false)
|
||||||
private var rpcError: JsonRpcError? = null
|
private var rpcError: JsonRpcError? = null
|
||||||
|
private var sig: ResponseSigner.Signature? = null
|
||||||
|
|
||||||
override fun init(head: Head) {
|
override fun init(head: Head) {
|
||||||
}
|
}
|
||||||
@@ -44,16 +46,17 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
|
|||||||
return failed.get()
|
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
|
val lagging = upstream.getLag() > maxLag
|
||||||
if (!lagging) {
|
if (!lagging) {
|
||||||
result.set(response)
|
result.set(response)
|
||||||
|
sig = signature
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: JsonRpcException, upstream: Upstream) {
|
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||||
this.rpcError = error.error
|
this.rpcError = error.error
|
||||||
val lagging = upstream.getLag() > maxLag
|
val lagging = upstream.getLag() > maxLag
|
||||||
if (!lagging && result.get() == null) {
|
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 {
|
override fun getResult(): ByteArray {
|
||||||
return result.get()
|
return result.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
|||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.ApiSource
|
import io.emeraldpay.dshackle.upstream.ApiSource
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
|
|
||||||
// creates instance of a Quorum based reader
|
// creates instance of a Quorum based reader
|
||||||
interface QuorumReaderFactory {
|
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 {
|
class Default : QuorumReaderFactory {
|
||||||
override fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
override fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||||
return QuorumRpcReader(apis, quorum)
|
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.JsonRpcException
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
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.RpcException
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.util.function.Tuple2
|
import reactor.util.function.Tuple2
|
||||||
|
import reactor.util.function.Tuple3
|
||||||
import reactor.util.function.Tuples
|
import reactor.util.function.Tuples
|
||||||
|
import java.util.Optional
|
||||||
import java.util.function.BiFunction
|
import java.util.function.BiFunction
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
@@ -36,13 +39,16 @@ import java.util.function.Function
|
|||||||
*/
|
*/
|
||||||
class QuorumRpcReader(
|
class QuorumRpcReader(
|
||||||
private val apiControl: ApiSource,
|
private val apiControl: ApiSource,
|
||||||
private val quorum: CallQuorum
|
private val quorum: CallQuorum,
|
||||||
|
private val signer: ResponseSigner?,
|
||||||
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
|
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(apiControl: ApiSource, quorum: CallQuorum) : this(apiControl, quorum, null)
|
||||||
|
|
||||||
override fun read(key: JsonRpcRequest): Mono<Result> {
|
override fun read(key: JsonRpcRequest): Mono<Result> {
|
||||||
// needs at least one response, so start a request
|
// needs at least one response, so start a request
|
||||||
apiControl.request(1)
|
apiControl.request(1)
|
||||||
@@ -83,8 +89,8 @@ class QuorumRpcReader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun execute(key: JsonRpcRequest, retrySpec: reactor.util.retry.Retry): Function<Flux<Upstream>, Mono<CallQuorum>> {
|
fun execute(key: JsonRpcRequest, retrySpec: reactor.util.retry.Retry): Function<Flux<Upstream>, Mono<CallQuorum>> {
|
||||||
val quorumReduce = BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { res, a ->
|
val quorumReduce = BiFunction<CallQuorum, Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>, CallQuorum> { res, a ->
|
||||||
if (res.record(a.t1, a.t2)) {
|
if (res.record(a.t1, a.t2.orElse(null), a.t3)) {
|
||||||
apiControl.resolve()
|
apiControl.resolve()
|
||||||
} else {
|
} else {
|
||||||
// quorum needs more responses, so ask api controller to make another
|
// 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
|
.filter { it.isResolved() } // return nothing if not resolved
|
||||||
.map {
|
.map {
|
||||||
// TODO find actual quorum number
|
// TODO find actual quorum number
|
||||||
QuorumRpcReader.Result(it.getResult()!!, 1)
|
QuorumRpcReader.Result(it.getResult()!!, it.getSignature(), 1)
|
||||||
}
|
}
|
||||||
.switchIfEmpty(defaultResult)
|
.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()
|
return api.getApi()
|
||||||
.read(key)
|
.read(key)
|
||||||
.flatMap { response ->
|
.flatMap { response ->
|
||||||
response.requireResult()
|
response.requireResult()
|
||||||
.onErrorResume { err ->
|
.transform(withSignature(api, key, response))
|
||||||
// on error notify quorum, it may use error message or other details
|
.transform(withErrorResume(api, key))
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.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> {
|
fun setupDefaultResult(key: JsonRpcRequest): Mono<Result> {
|
||||||
@@ -162,6 +191,7 @@ class QuorumRpcReader(
|
|||||||
|
|
||||||
class Result(
|
class Result(
|
||||||
val value: ByteArray,
|
val value: ByteArray,
|
||||||
|
val signature: ResponseSigner.Signature?,
|
||||||
val quorum: Int
|
val quorum: Int
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.Global
|
|||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import io.emeraldpay.etherjar.rpc.RpcException
|
import io.emeraldpay.etherjar.rpc.RpcException
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@@ -34,26 +35,26 @@ abstract class ValueAwareQuorum<T>(
|
|||||||
return Global.objectMapper.readValue(response.inputStream(), clazz)
|
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 {
|
try {
|
||||||
val value = extractValue(response, clazz)
|
val value = extractValue(response, clazz)
|
||||||
recordValue(response, value, upstream)
|
recordValue(response, value, signature, upstream)
|
||||||
} catch (e: RpcException) {
|
} catch (e: RpcException) {
|
||||||
recordError(response, e.rpcMessage, upstream)
|
recordError(response, e.rpcMessage, signature, upstream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
recordError(response, e.message, upstream)
|
recordError(response, e.message, signature, upstream)
|
||||||
}
|
}
|
||||||
return isResolved()
|
return isResolved()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(error: JsonRpcException, upstream: Upstream) {
|
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||||
this.rpcError = error.error
|
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? {
|
override fun getError(): JsonRpcError? {
|
||||||
return rpcError
|
return rpcError
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class BlockchainRpc(
|
|||||||
@Autowired private val trackAddress: List<TrackAddress>,
|
@Autowired private val trackAddress: List<TrackAddress>,
|
||||||
@Autowired private val describe: Describe,
|
@Autowired private val describe: Describe,
|
||||||
@Autowired private val subscribeStatus: SubscribeStatus,
|
@Autowired private val subscribeStatus: SubscribeStatus,
|
||||||
@Autowired private val estimateFee: EstimateFee
|
@Autowired private val estimateFee: EstimateFee,
|
||||||
) : ReactorBlockchainGrpc.BlockchainImplBase() {
|
) : ReactorBlockchainGrpc.BlockchainImplBase() {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
|
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.JsonRpcException
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
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.RpcException
|
||||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||||
import io.emeraldpay.grpc.BlockchainType
|
import io.emeraldpay.grpc.BlockchainType
|
||||||
@@ -49,7 +50,8 @@ import java.util.EnumMap
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
open class NativeCall(
|
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)
|
private val log = LoggerFactory.getLogger(NativeCall::class.java)
|
||||||
@@ -85,7 +87,7 @@ open class NativeCall(
|
|||||||
} else {
|
} else {
|
||||||
val error = it.getError()
|
val error = it.getError()
|
||||||
Mono.just(
|
Mono.just(
|
||||||
CallResult(error.id, null, error)
|
CallResult(error.id, 0, null, error, null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,10 +109,21 @@ open class NativeCall(
|
|||||||
} else {
|
} else {
|
||||||
result.payload = ByteString.copyFrom(it.result)
|
result.payload = ByteString.copyFrom(it.result)
|
||||||
}
|
}
|
||||||
|
if (it.nonce != null && it.signature != null) {
|
||||||
|
result.signature = buildSignature(it.nonce, it.signature)
|
||||||
|
}
|
||||||
return result.build()
|
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> {
|
fun processException(it: Throwable?): Mono<BlockchainOuterClass.NativeCallReplyItem> {
|
||||||
val id: Int = if (it != null && it is CallFailure) {
|
val id: Int = if (it != null && it is CallFailure) {
|
||||||
it.id
|
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
|
// for ethereum the actual block needed for the call may be specified in the call parameters
|
||||||
val callSpecificMatcher: Mono<Selector.Matcher> =
|
val callSpecificMatcher: Mono<Selector.Matcher> =
|
||||||
if (BlockchainType.from(upstream.chain) == BlockchainType.ETHEREUM) {
|
if (BlockchainType.from(upstream.chain) == BlockchainType.ETHEREUM) {
|
||||||
@@ -179,7 +191,6 @@ open class NativeCall(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
} ?: Mono.empty()
|
} ?: Mono.empty()
|
||||||
|
|
||||||
return callSpecificMatcher.defaultIfEmpty(Selector.empty).map { csm ->
|
return callSpecificMatcher.defaultIfEmpty(Selector.empty).map { csm ->
|
||||||
val matcher = Selector.Builder()
|
val matcher = Selector.Builder()
|
||||||
.withMatcher(csm)
|
.withMatcher(csm)
|
||||||
@@ -196,27 +207,27 @@ open class NativeCall(
|
|||||||
val heightMatcher = Selector.HeightMatcher(minHeight)
|
val heightMatcher = Selector.HeightMatcher(minHeight)
|
||||||
matcher.withMatcher(heightMatcher)
|
matcher.withMatcher(heightMatcher)
|
||||||
}
|
}
|
||||||
|
val nonce = requestItem.nonce.let { if (it == 0L) null else it }
|
||||||
ValidCallContext(requestItem.id, upstream, matcher.build(), callQuorum, RawCallDetails(method, params))
|
ValidCallContext(requestItem.id, nonce, upstream, matcher.build(), callQuorum, RawCallDetails(method, params))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> {
|
fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> {
|
||||||
return ctx.upstream.getRoutedApi(ctx.matcher)
|
return ctx.upstream.getRoutedApi(ctx.matcher)
|
||||||
.flatMap { api ->
|
.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)
|
.flatMap(JsonRpcResponse::requireResult)
|
||||||
.map {
|
.map {
|
||||||
CallResult.ok(ctx.id, it)
|
CallResult.ok(ctx.id, ctx.nonce, it, null)
|
||||||
}
|
}
|
||||||
}.switchIfEmpty(
|
}.switchIfEmpty(
|
||||||
Mono.just(ctx).flatMap(this::executeOnRemote)
|
Mono.just(ctx).flatMap(this::executeOnRemote)
|
||||||
)
|
)
|
||||||
.onErrorResume {
|
.onErrorResume {
|
||||||
if (it is CallFailure) {
|
if (it is CallFailure) {
|
||||||
Mono.just(CallResult.fail(it.id, it.reason))
|
Mono.just(CallResult.fail(it.id, ctx.nonce, it.reason))
|
||||||
} else {
|
} 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)) {
|
if (!ctx.upstream.getMethods().isCallable(ctx.payload.method)) {
|
||||||
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported 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
|
return reader
|
||||||
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce))
|
||||||
.map {
|
.map {
|
||||||
CallResult(ctx.id, it.value, null)
|
CallResult(ctx.id, ctx.nonce, it.value, null, it.signature)
|
||||||
}
|
}
|
||||||
.onErrorResume { t ->
|
.onErrorResume { t ->
|
||||||
val failure = when (t) {
|
val failure = when (t) {
|
||||||
is CallFailure -> CallResult.fail(t.id, t.reason)
|
is CallFailure -> CallResult.fail(t.id, ctx.nonce, t.reason)
|
||||||
is JsonRpcException -> CallResult.fail(ctx.id, t.error.code, t.error.message)
|
is JsonRpcException -> CallResult.fail(ctx.id, ctx.nonce, t.error.code, t.error.message)
|
||||||
else -> CallResult.fail(ctx.id, t)
|
else -> CallResult.fail(ctx.id, ctx.nonce, t)
|
||||||
}
|
}
|
||||||
Mono.just(failure)
|
Mono.just(failure)
|
||||||
}
|
}
|
||||||
.switchIfEmpty(
|
.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>(
|
open class ValidCallContext<T>(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val nonce: Long?,
|
||||||
val upstream: Multistream,
|
val upstream: Multistream,
|
||||||
val matcher: Selector.Matcher,
|
val matcher: Selector.Matcher,
|
||||||
val callQuorum: CallQuorum,
|
val callQuorum: CallQuorum,
|
||||||
@@ -280,7 +292,7 @@ open class NativeCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <X> withPayload(payload: X): ValidCallContext<X> {
|
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 {
|
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 {
|
companion object {
|
||||||
fun ok(id: Int, result: ByteArray): CallResult {
|
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?): CallResult {
|
||||||
return CallResult(id, result, null)
|
return CallResult(id, nonce, result, null, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fail(id: Int, errorCore: Int, errorMessage: String): CallResult {
|
fun fail(id: Int, nonce: Long?, errorCore: Int, errorMessage: String): CallResult {
|
||||||
return CallResult(id, null, CallError(errorCore, errorMessage, null))
|
return CallResult(id, nonce, null, CallError(errorCore, errorMessage, null), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fail(id: Int, error: Throwable): CallResult {
|
fun fail(id: Int, nonce: Long?, error: Throwable): CallResult {
|
||||||
return CallResult(id, null, CallError.from(error))
|
return CallResult(id, nonce, null, CallError.from(error), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.rpc
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
@@ -155,7 +154,6 @@ class TrackBitcoinTx(
|
|||||||
Common.BlockInfo.newBuilder()
|
Common.BlockInfo.newBuilder()
|
||||||
.setBlockId(tx.blockHash!!.substring(2))
|
.setBlockId(tx.blockHash!!.substring(2))
|
||||||
.setTimestamp(tx.blockTime!!.toEpochMilli())
|
.setTimestamp(tx.blockTime!!.toEpochMilli())
|
||||||
.setWeight(ByteString.copyFrom(tx.blockTotalDifficulty!!.toByteArray()))
|
|
||||||
.setHeight(tx.height!!)
|
.setHeight(tx.height!!)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.rpc
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.dshackle.SilentException
|
import io.emeraldpay.dshackle.SilentException
|
||||||
@@ -258,7 +257,6 @@ class TrackEthereumTx(
|
|||||||
Common.BlockInfo.newBuilder()
|
Common.BlockInfo.newBuilder()
|
||||||
.setBlockId(tx.status.blockHash!!.toHex().substring(2))
|
.setBlockId(tx.status.blockHash!!.toHex().substring(2))
|
||||||
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
|
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
|
||||||
.setWeight(ByteString.copyFrom(tx.status.blockTotalDifficulty!!.toByteArray()))
|
|
||||||
.setHeight(tx.status.height!!)
|
.setHeight(tx.status.height!!)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ abstract class DefaultUpstream(
|
|||||||
.multicast()
|
.multicast()
|
||||||
.directBestEffort<UpstreamAvailability>()
|
.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 {
|
override fun isAvailable(): Boolean {
|
||||||
return getStatus() == UpstreamAvailability.OK
|
return getStatus() == UpstreamAvailability.OK
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,8 @@ class EthereumDirectReader(
|
|||||||
*/
|
*/
|
||||||
private fun readWithQuorum(request: JsonRpcRequest): Mono<ByteArray> {
|
private fun readWithQuorum(request: JsonRpcRequest): Mono<ByteArray> {
|
||||||
return quorumReaderFactory
|
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)
|
.read(request)
|
||||||
.map { it.value }
|
.map { it.value }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,14 @@ class LocalCallRouter(
|
|||||||
return Mono.just(methods.executeHardcoded(key.method))
|
return Mono.just(methods.executeHardcoded(key.method))
|
||||||
.map { JsonRpcResponse(it, null) }
|
.map { JsonRpcResponse(it, null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!methods.isCallable(key.method)) {
|
if (!methods.isCallable(key.method)) {
|
||||||
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported 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)
|
val common = commonRequests(key)
|
||||||
if (common != null) {
|
if (common != null) {
|
||||||
return common.map { JsonRpcResponse(it, null) }
|
return common.map { JsonRpcResponse(it, null) }
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ class WsConnection(
|
|||||||
fun onRpc(msg: ResponseWSParser.WsResponse): Mono<Void> {
|
fun onRpc(msg: ResponseWSParser.WsResponse): Mono<Void> {
|
||||||
return if (msg.id.isNumber()) {
|
return if (msg.id.isNumber()) {
|
||||||
val resp = JsonRpcResponse(
|
val resp = JsonRpcResponse(
|
||||||
msg.value, msg.error, msg.id
|
msg.value, msg.error, msg.id, null
|
||||||
)
|
)
|
||||||
Mono.fromCallable {
|
Mono.fromCallable {
|
||||||
val status = rpcReceive.tryEmitNext(resp)
|
val status = rpcReceive.tryEmitNext(resp)
|
||||||
@@ -377,7 +377,7 @@ class WsConnection(
|
|||||||
RpcResponseError.CODE_INTERNAL_ERROR,
|
RpcResponseError.CODE_INTERNAL_ERROR,
|
||||||
"Response not received from WebSocket"
|
"Response not received from WebSocket"
|
||||||
),
|
),
|
||||||
JsonRpcResponse.Id.from(originalId)
|
JsonRpcResponse.Id.from(originalId), null
|
||||||
)
|
)
|
||||||
|
|
||||||
return Flux.from(rpcReceive.asFlux())
|
return Flux.from(rpcReceive.asFlux())
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import org.springframework.context.Lifecycle
|
|||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ class BitcoinGrpcUpstream(
|
|||||||
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val client: JsonRpcGrpcClient
|
private val client: JsonRpcGrpcClient
|
||||||
) : BitcoinUpstream(
|
) : BitcoinUpstream(
|
||||||
"$parentId/${chain.chainCode}",
|
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
|
||||||
chain,
|
chain,
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
role
|
role
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import org.springframework.context.Lifecycle
|
|||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ open class EthereumGrpcUpstream(
|
|||||||
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||||
private val client: JsonRpcGrpcClient
|
private val client: JsonRpcGrpcClient
|
||||||
) : EthereumUpstream(
|
) : EthereumUpstream(
|
||||||
"$parentId/${chain.chainCode}",
|
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
|
||||||
UpstreamsConfig.Options.getDefaults(),
|
UpstreamsConfig.Options.getDefaults(),
|
||||||
role,
|
role,
|
||||||
null, null
|
null, null
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ package io.emeraldpay.dshackle.upstream.rpcclient
|
|||||||
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallReplySignature
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
import io.emeraldpay.dshackle.reader.Reader
|
import io.emeraldpay.dshackle.reader.Reader
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import io.emeraldpay.etherjar.rpc.RpcException
|
import io.emeraldpay.etherjar.rpc.RpcException
|
||||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
@@ -60,13 +62,14 @@ class JsonRpcGrpcClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
val reqItem = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||||
.setId(1)
|
.setId(1)
|
||||||
.setMethod(key.method)
|
.setMethod(key.method)
|
||||||
.setPayload(ByteString.copyFrom(Global.objectMapper.writeValueAsBytes(key.params)))
|
.setPayload(ByteString.copyFrom(Global.objectMapper.writeValueAsBytes(key.params)))
|
||||||
.build().let {
|
if (key.nonce != null) {
|
||||||
req.addItems(it)
|
reqItem.nonce = key.nonce
|
||||||
}
|
}
|
||||||
|
req.addItems(reqItem.build())
|
||||||
|
|
||||||
return Mono.just(key)
|
return Mono.just(key)
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
@@ -77,7 +80,12 @@ class JsonRpcGrpcClient(
|
|||||||
.flatMap { resp ->
|
.flatMap { resp ->
|
||||||
if (resp.succeed) {
|
if (resp.succeed) {
|
||||||
val bytes = resp.payload.toByteArray()
|
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 {
|
} else {
|
||||||
metrics.fails.increment()
|
metrics.fails.increment()
|
||||||
Mono.error(
|
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(
|
data class JsonRpcRequest(
|
||||||
val method: String,
|
val method: String,
|
||||||
val params: List<Any?>,
|
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 {
|
fun toJson(): ByteArray {
|
||||||
val json = mapOf(
|
val json = mapOf(
|
||||||
@@ -62,7 +63,7 @@ data class JsonRpcRequest(
|
|||||||
throw IllegalStateException("Unsupported param type: ${it.asToken()}")
|
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.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
import com.fasterxml.jackson.databind.JsonSerializer
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
import com.fasterxml.jackson.databind.SerializerProvider
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
class JsonRpcResponse(
|
class JsonRpcResponse(
|
||||||
private val result: ByteArray?,
|
private val result: ByteArray?,
|
||||||
val error: JsonRpcError?,
|
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))
|
constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, NumberId(0))
|
||||||
@@ -107,7 +113,11 @@ class JsonRpcResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun copyWithId(id: Id): 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 {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ open class ResponseRpcParser : ResponseParser<JsonRpcResponse>() {
|
|||||||
|
|
||||||
override fun build(state: Preparsed): JsonRpcResponse {
|
override fun build(state: Preparsed): JsonRpcResponse {
|
||||||
if (state.error != null) {
|
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) {
|
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.OpenSsl
|
||||||
import io.netty.handler.ssl.OpenSslServerContext
|
import io.netty.handler.ssl.OpenSslServerContext
|
||||||
import io.netty.handler.ssl.SslContext
|
import io.netty.handler.ssl.SslContext
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
import sun.security.x509.X509CertImpl
|
import sun.security.x509.X509CertImpl
|
||||||
|
|
||||||
|
import java.security.Security
|
||||||
|
|
||||||
class TlsSetupSpec extends Specification {
|
class TlsSetupSpec extends Specification {
|
||||||
|
|
||||||
TlsSetup tlsSetup = new TlsSetup(new FileResolver(new File("src/test/resources/tls-local")))
|
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
|
// run test on OS with OpenSSL installed
|
||||||
// !!!!!!!!!!!!
|
// !!!!!!!!!!!!
|
||||||
@@ -148,12 +153,13 @@ class TlsSetupSpec extends Specification {
|
|||||||
def config = new AuthConfig.ServerTlsAuth(
|
def config = new AuthConfig.ServerTlsAuth(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
certificate: "127.0.0.1.crt",
|
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:
|
when:
|
||||||
tlsSetup.setupServer("test", config, false)
|
tlsSetup.setupServer("test", config, false)
|
||||||
then:
|
then:
|
||||||
def t = thrown(IllegalArgumentException)
|
thrown(Exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Fail if client certificate not set but required"() {
|
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)
|
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||||
call.items.add(request)
|
call.items.add(request)
|
||||||
call.ids[0] = 5
|
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:
|
when:
|
||||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
|
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
|
||||||
.collectList()
|
.collectList()
|
||||||
@@ -85,7 +85,7 @@ class BaseHandlerSpec extends Specification {
|
|||||||
def call = new ProxyCall(ProxyCall.RpcType.BATCH)
|
def call = new ProxyCall(ProxyCall.RpcType.BATCH)
|
||||||
call.items.add(request)
|
call.items.add(request)
|
||||||
call.ids[0] = 5
|
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:
|
when:
|
||||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
|
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
|
||||||
.collectList()
|
.collectList()
|
||||||
@@ -116,8 +116,8 @@ class BaseHandlerSpec extends Specification {
|
|||||||
call.items.add(request2)
|
call.items.add(request2)
|
||||||
call.ids[1] = 6
|
call.ids[1] = 6
|
||||||
def response = [
|
def response = [
|
||||||
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
|
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
|
||||||
new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
||||||
@@ -149,8 +149,8 @@ class BaseHandlerSpec extends Specification {
|
|||||||
call.items.add(request2)
|
call.items.add(request2)
|
||||||
call.ids[1] = 6
|
call.ids[1] = 6
|
||||||
def response = [
|
def response = [
|
||||||
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
|
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
|
||||||
new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
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
|
// note there is only 2 responses
|
||||||
def response = [
|
def response = [
|
||||||
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
|
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
|
||||||
new NativeCall.CallResult(2, '{"foo": 3}'.bytes, null)
|
new NativeCall.CallResult(2, null, '{"foo": 3}'.bytes, null, null)
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class HttpHandlerSpec extends Specification {
|
|||||||
.setMethod("test_test")
|
.setMethod("test_test")
|
||||||
.setPayload(ByteString.copyFromUtf8("[]"))
|
.setPayload(ByteString.copyFromUtf8("[]"))
|
||||||
.build()
|
.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()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||||
.addItems(reqItem)
|
.addItems(reqItem)
|
||||||
@@ -129,7 +129,7 @@ class HttpHandlerSpec extends Specification {
|
|||||||
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler(), false)
|
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler(), false)
|
||||||
|
|
||||||
then:
|
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)
|
StepVerifier.create(act)
|
||||||
.expectNext("hello")
|
.expectNext("hello")
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class WebsocketHandlerSpec extends Specification {
|
|||||||
|
|
||||||
def "Respond to a single call"() {
|
def "Respond to a single call"() {
|
||||||
setup:
|
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) {
|
def nativeCall = Mock(NativeCall) {
|
||||||
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])
|
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class WriteRpcJsonSpec extends Specification {
|
|||||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||||
call.ids[1] = 105
|
call.ids[1] = 105
|
||||||
def data = [
|
def data = [
|
||||||
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
|
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null)
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = writer.toJson(call, data[0])
|
def act = writer.toJson(call, data[0])
|
||||||
@@ -98,7 +98,7 @@ class WriteRpcJsonSpec extends Specification {
|
|||||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||||
call.ids[1] = 1
|
call.ids[1] = 1
|
||||||
def data = [
|
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:
|
when:
|
||||||
def act = writer.toJson(call, data[0])
|
def act = writer.toJson(call, data[0])
|
||||||
@@ -111,7 +111,7 @@ class WriteRpcJsonSpec extends Specification {
|
|||||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||||
call.ids[1] = "aaa"
|
call.ids[1] = "aaa"
|
||||||
def data = [
|
def data = [
|
||||||
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
|
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null)
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = writer.toJson(call, data[0])
|
def act = writer.toJson(call, data[0])
|
||||||
@@ -126,9 +126,9 @@ class WriteRpcJsonSpec extends Specification {
|
|||||||
call.ids[2] = 11
|
call.ids[2] = 11
|
||||||
call.ids[3] = 15
|
call.ids[3] = 15
|
||||||
def data = [
|
def data = [
|
||||||
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null),
|
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null),
|
||||||
new NativeCall.CallResult(2, null, new NativeCall.CallError(2, "oops", null)),
|
new NativeCall.CallResult(2, null, null, new NativeCall.CallError(2, "oops", null), null),
|
||||||
new NativeCall.CallResult(3, '{"hash": "0x2484f459dc"}'.bytes, null),
|
new NativeCall.CallResult(3, null, '{"hash": "0x2484f459dc"}'.bytes, null, null),
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = Flux.fromIterable(data)
|
def act = Flux.fromIterable(data)
|
||||||
@@ -154,7 +154,7 @@ class WriteRpcJsonSpec extends Specification {
|
|||||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||||
call.ids[1] = 10
|
call.ids[1] = 10
|
||||||
def data = [
|
def data = [
|
||||||
new NativeCall.CallResult(1, '"0x1"'.bytes, null),
|
new NativeCall.CallResult(1, null, '"0x1"'.bytes, null, null),
|
||||||
]
|
]
|
||||||
when:
|
when:
|
||||||
def act = Flux.fromIterable(data)
|
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.Upstream
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
class AlwaysQuorumSpec extends Specification {
|
class AlwaysQuorumSpec extends Specification {
|
||||||
@@ -26,7 +27,7 @@ class AlwaysQuorumSpec extends Specification {
|
|||||||
def quorum = new AlwaysQuorum()
|
def quorum = new AlwaysQuorum()
|
||||||
def up = Stub(Upstream)
|
def up = Stub(Upstream)
|
||||||
when:
|
when:
|
||||||
quorum.record(new JsonRpcException(1, "test"), up)
|
quorum.record(new JsonRpcException(1, "test"), null, up)
|
||||||
then:
|
then:
|
||||||
quorum.isFailed()
|
quorum.isFailed()
|
||||||
!quorum.isResolved()
|
!quorum.isResolved()
|
||||||
@@ -41,10 +42,11 @@ class AlwaysQuorumSpec extends Specification {
|
|||||||
def quorum = new AlwaysQuorum()
|
def quorum = new AlwaysQuorum()
|
||||||
def up = Stub(Upstream)
|
def up = Stub(Upstream)
|
||||||
when:
|
when:
|
||||||
quorum.record("123".bytes, up)
|
quorum.record("123".bytes, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
|
||||||
then:
|
then:
|
||||||
quorum.isResolved()
|
quorum.isResolved()
|
||||||
quorum.getResult() == "123".bytes
|
quorum.getResult() == "123".bytes
|
||||||
|
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
|
||||||
!quorum.isFailed()
|
!quorum.isFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,21 +43,21 @@ class BroadcastQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1)
|
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
|
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _, _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||||
}
|
}
|
||||||
@@ -75,21 +75,21 @@ class BroadcastQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal error"), upstream1)
|
q.record(new JsonRpcException(1, "Internal error"), null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
|
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _, _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||||
}
|
}
|
||||||
@@ -99,19 +99,19 @@ class BroadcastQuorumSpec extends Specification {
|
|||||||
def quorum = new BroadcastQuorum(3)
|
def quorum = new BroadcastQuorum(3)
|
||||||
def up = Stub(Upstream)
|
def up = Stub(Upstream)
|
||||||
when:
|
when:
|
||||||
quorum.record(new JsonRpcException(1, "test 1"), up)
|
quorum.record(new JsonRpcException(1, "test 1"), null, up)
|
||||||
then:
|
then:
|
||||||
!quorum.isFailed()
|
!quorum.isFailed()
|
||||||
!quorum.isResolved()
|
!quorum.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
quorum.record(new JsonRpcException(1, "test 2"), up)
|
quorum.record(new JsonRpcException(1, "test 2"), null, up)
|
||||||
then:
|
then:
|
||||||
!quorum.isFailed()
|
!quorum.isFailed()
|
||||||
!quorum.isResolved()
|
!quorum.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
quorum.record(new JsonRpcException(1, "test 3"), up)
|
quorum.record(new JsonRpcException(1, "test 3"), null, up)
|
||||||
then:
|
then:
|
||||||
quorum.isFailed()
|
quorum.isFailed()
|
||||||
!quorum.isResolved()
|
!quorum.isResolved()
|
||||||
|
|||||||
@@ -38,22 +38,23 @@ class NonEmptyQuorumSpec extends Specification {
|
|||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream2)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream3)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
|
||||||
then:
|
then:
|
||||||
q.isFailed()
|
q.isFailed()
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
q.signature == null
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Fail first if not error"() {
|
def "Fail first if not error"() {
|
||||||
@@ -70,7 +71,7 @@ class NonEmptyQuorumSpec extends Specification {
|
|||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x11"'.bytes, upstream1)
|
q.record('"0x11"'.bytes, null, upstream1)
|
||||||
then:
|
then:
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
@@ -90,14 +91,14 @@ class NonEmptyQuorumSpec extends Specification {
|
|||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
q.signature == null
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x11"'.bytes, upstream2)
|
q.record('"0x11"'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
@@ -117,14 +118,14 @@ class NonEmptyQuorumSpec extends Specification {
|
|||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('null'.bytes, upstream2)
|
q.record('null'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x11"'.bytes, upstream2)
|
q.record('"0x11"'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|||||||
@@ -43,21 +43,21 @@ class NonceQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x10"'.bytes, upstream1)
|
q.record('"0x10"'.bytes, null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0x10", _)
|
1 * q.recordValue(_, "0x10", _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x11"'.bytes, upstream2)
|
q.record('"0x11"'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0x11", _)
|
1 * q.recordValue(_, "0x11", _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x10"'.bytes, upstream3)
|
q.record('"0x10"'.bytes, null, upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordValue(_, "0x10", _)
|
1 * q.recordValue(_, "0x10", _, _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Object) == "0x11"
|
objectMapper.readValue(q.result, Object) == "0x11"
|
||||||
}
|
}
|
||||||
@@ -75,27 +75,27 @@ class NonceQuorumSpec extends Specification {
|
|||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordError(_, _, _)
|
1 * q.recordError(_, _, _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x11"'.bytes, upstream2)
|
q.record('"0x11"'.bytes, null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
1 * q.recordValue(_, "0x11", _)
|
1 * q.recordValue(_, "0x11", _, _)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x10"'.bytes, upstream3)
|
q.record('"0x10"'.bytes, null, upstream3)
|
||||||
then:
|
then:
|
||||||
1 * q.recordValue(_, "0x10", _)
|
1 * q.recordValue(_, "0x10", _, _)
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record('"0x11"'.bytes, upstream1)
|
q.record('"0x11"'.bytes, null, upstream1)
|
||||||
then:
|
then:
|
||||||
1 * q.recordValue(_, "0x11", _)
|
1 * q.recordValue(_, "0x11", _, _)
|
||||||
q.isResolved()
|
q.isResolved()
|
||||||
objectMapper.readValue(q.result, Object) == "0x11"
|
objectMapper.readValue(q.result, Object) == "0x11"
|
||||||
}
|
}
|
||||||
@@ -114,23 +114,24 @@ class NonceQuorumSpec extends Specification {
|
|||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream2)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
|
||||||
then:
|
then:
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
!q.isFailed()
|
!q.isFailed()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
q.record(new JsonRpcException(1, "Internal"), upstream3)
|
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
|
||||||
then:
|
then:
|
||||||
q.isFailed()
|
q.isFailed()
|
||||||
!q.isResolved()
|
!q.isResolved()
|
||||||
q.getError() != null
|
q.getError() != null
|
||||||
q.getError().message == "Internal"
|
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.upstream.Upstream
|
||||||
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
|
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import io.emeraldpay.etherjar.rpc.RpcException
|
import io.emeraldpay.etherjar.rpc.RpcException
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class NotLaggingQuorumSpec extends Specification {
|
|||||||
def quorum = new NotLaggingQuorum(1)
|
def quorum = new NotLaggingQuorum(1)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
quorum.record(value, up)
|
quorum.record(value, null, up)
|
||||||
then:
|
then:
|
||||||
1 * up.getLag() >> 0
|
1 * up.getLag() >> 0
|
||||||
quorum.isResolved()
|
quorum.isResolved()
|
||||||
@@ -39,6 +40,22 @@ class NotLaggingQuorumSpec extends Specification {
|
|||||||
quorum.result == value
|
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"() {
|
def "Resolves if ok lag"() {
|
||||||
setup:
|
setup:
|
||||||
def up = Mock(Upstream)
|
def up = Mock(Upstream)
|
||||||
@@ -46,7 +63,7 @@ class NotLaggingQuorumSpec extends Specification {
|
|||||||
def quorum = new NotLaggingQuorum(1)
|
def quorum = new NotLaggingQuorum(1)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
quorum.record(value, up)
|
quorum.record(value, null, up)
|
||||||
then:
|
then:
|
||||||
1 * up.getLag() >> 1
|
1 * up.getLag() >> 1
|
||||||
quorum.isResolved()
|
quorum.isResolved()
|
||||||
@@ -61,7 +78,7 @@ class NotLaggingQuorumSpec extends Specification {
|
|||||||
def quorum = new NotLaggingQuorum(1)
|
def quorum = new NotLaggingQuorum(1)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
quorum.record(value, up)
|
quorum.record(value, null, up)
|
||||||
then:
|
then:
|
||||||
1 * up.getLag() >> 2
|
1 * up.getLag() >> 2
|
||||||
!quorum.isResolved()
|
!quorum.isResolved()
|
||||||
@@ -75,7 +92,7 @@ class NotLaggingQuorumSpec extends Specification {
|
|||||||
def quorum = new NotLaggingQuorum(1)
|
def quorum = new NotLaggingQuorum(1)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
quorum.record(new JsonRpcException(-100, "test error"), up)
|
quorum.record(new JsonRpcException(-100, "test error"), null, up)
|
||||||
then:
|
then:
|
||||||
1 * up.getLag() >> 1
|
1 * up.getLag() >> 1
|
||||||
!quorum.isResolved()
|
!quorum.isResolved()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.quorum
|
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.Head
|
||||||
import io.emeraldpay.dshackle.upstream.Upstream
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
import org.jetbrains.annotations.NotNull
|
import org.jetbrains.annotations.NotNull
|
||||||
@@ -66,12 +66,14 @@ class ValueAwareQuorumSpec extends Specification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
@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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
ResponseSigner.Signature getSignature() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
byte[] getResult() {
|
byte[] getResult() {
|
||||||
return new byte[0]
|
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.Multistream
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
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.DefaultEthereumMethods
|
||||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import io.emeraldpay.etherjar.rpc.RpcException
|
import io.emeraldpay.etherjar.rpc.RpcException
|
||||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||||
@@ -51,6 +51,16 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
ObjectMapper objectMapper = Global.objectMapper
|
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 "Tries router first"() {
|
||||||
def routedApi = Mock(Reader) {
|
def routedApi = Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
|
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) {
|
def upstream = Mock(Multistream) {
|
||||||
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||||
}
|
}
|
||||||
def upstreams = Stub(MultistreamHolder)
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams)
|
def nativeCall = nativeCall()
|
||||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(
|
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", [])
|
new NativeCall.ParsedCallDetails("eth_test", [])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,6 +81,7 @@ class NativeCallSpec extends Specification {
|
|||||||
act.result == "1".bytes
|
act.result == "1".bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def "Return error if router denied the requests"() {
|
def "Return error if router denied the requests"() {
|
||||||
def routedApi = Mock(Reader) {
|
def routedApi = Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message"))
|
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) {
|
def upstream = Mock(Multistream) {
|
||||||
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||||
}
|
}
|
||||||
def upstreams = Stub(MultistreamHolder)
|
|
||||||
|
|
||||||
def nativeCall = new NativeCall(upstreams)
|
def nativeCall = nativeCall()
|
||||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(
|
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", [])
|
new NativeCall.ParsedCallDetails("eth_test", [])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -102,13 +111,13 @@ class NativeCallSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def quorum = new AlwaysQuorum()
|
def quorum = new AlwaysQuorum()
|
||||||
|
|
||||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
def nativeCall = nativeCall()
|
||||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1))
|
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", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -116,19 +125,20 @@ class NativeCallSpec extends Specification {
|
|||||||
def act = objectMapper.readValue(resp.result, Object)
|
def act = objectMapper.readValue(resp.result, Object)
|
||||||
then:
|
then:
|
||||||
act == "foo"
|
act == "foo"
|
||||||
|
resp.nonce == 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Returns error if no quorum"() {
|
def "Returns error if no quorum"() {
|
||||||
setup:
|
setup:
|
||||||
def quorum = new AlwaysQuorum()
|
def quorum = new AlwaysQuorum()
|
||||||
|
|
||||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
def nativeCall = nativeCall()
|
||||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(_) >> Mono.empty()
|
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", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -136,7 +146,7 @@ class NativeCallSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
StepVerifier.create(resp)
|
StepVerifier.create(resp)
|
||||||
.expectNextMatches { result ->
|
.expectNextMatches { result ->
|
||||||
result.isError()
|
result.isError() && result.nonce == 10
|
||||||
}
|
}
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
@@ -144,8 +154,7 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Packs call exception into response with id"() {
|
def "Packs call exception into response with id"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(MultistreamHolder)
|
def nativeCall = nativeCall()
|
||||||
def nativeCall = new NativeCall(upstreams)
|
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
|
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
|
||||||
then:
|
then:
|
||||||
@@ -161,8 +170,7 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Packs unknown exception into response"() {
|
def "Packs unknown exception into response"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(MultistreamHolder)
|
def nativeCall = nativeCall()
|
||||||
def nativeCall = new NativeCall(upstreams)
|
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
|
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
|
||||||
then:
|
then:
|
||||||
@@ -177,13 +185,12 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Builds normal response"() {
|
def "Builds normal response"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(MultistreamHolder)
|
def nativeCall = nativeCall()
|
||||||
def nativeCall = new NativeCall(upstreams)
|
|
||||||
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
||||||
|
|
||||||
when:
|
when:
|
||||||
def resp = nativeCall.buildResponse(
|
def resp = nativeCall.buildResponse(
|
||||||
new NativeCall.CallResult(1561, objectMapper.writeValueAsBytes(json), null)
|
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, null)
|
||||||
)
|
)
|
||||||
then:
|
then:
|
||||||
resp.id == 1561
|
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"]
|
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"() {
|
def "Returns error for invalid chain"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(MultistreamHolder)
|
def nativeCall = nativeCall()
|
||||||
def nativeCall = new NativeCall(upstreams)
|
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChainValue(0)
|
.setChainValue(0)
|
||||||
@@ -210,7 +235,6 @@ class NativeCallSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
StepVerifier.create(resp)
|
StepVerifier.create(resp)
|
||||||
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 0})
|
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 0})
|
||||||
// .expectComplete()
|
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +243,7 @@ class NativeCallSpec extends Specification {
|
|||||||
def upstreams = Mock(MultistreamHolder) {
|
def upstreams = Mock(MultistreamHolder) {
|
||||||
_ * it.observeChains() >> Flux.empty()
|
_ * it.observeChains() >> Flux.empty()
|
||||||
}
|
}
|
||||||
def nativeCall = new NativeCall(upstreams)
|
def nativeCall = nativeCall(upstreams)
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChainValue(Chain.TESTNET_MORDEN.id)
|
.setChainValue(Chain.TESTNET_MORDEN.id)
|
||||||
@@ -245,13 +269,14 @@ class NativeCallSpec extends Specification {
|
|||||||
def upstreams = Mock(MultistreamHolder) {
|
def upstreams = Mock(MultistreamHolder) {
|
||||||
_ * it.observeChains() >> Flux.empty()
|
_ * it.observeChains() >> Flux.empty()
|
||||||
}
|
}
|
||||||
def nativeCall = new NativeCall(upstreams)
|
def nativeCall = nativeCall(upstreams)
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||||
.addItems(
|
.addItems(
|
||||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||||
.setId(1)
|
.setId(1)
|
||||||
|
.setNonce(10)
|
||||||
.setMethod("eth_test")
|
.setMethod("eth_test")
|
||||||
.setPayload(ByteString.copyFromUtf8("[]"))
|
.setPayload(ByteString.copyFromUtf8("[]"))
|
||||||
)
|
)
|
||||||
@@ -263,6 +288,7 @@ class NativeCallSpec extends Specification {
|
|||||||
act.size() == 1
|
act.size() == 1
|
||||||
with(act[0]) {
|
with(act[0]) {
|
||||||
id == 1
|
id == 1
|
||||||
|
nonce == 10
|
||||||
payload.method == "eth_test"
|
payload.method == "eth_test"
|
||||||
payload.params == "[]"
|
payload.params == "[]"
|
||||||
}
|
}
|
||||||
@@ -273,7 +299,7 @@ class NativeCallSpec extends Specification {
|
|||||||
def upstreams = Mock(MultistreamHolder) {
|
def upstreams = Mock(MultistreamHolder) {
|
||||||
_ * it.observeChains() >> Flux.empty()
|
_ * it.observeChains() >> Flux.empty()
|
||||||
}
|
}
|
||||||
def nativeCall = new NativeCall(upstreams)
|
def nativeCall = nativeCall(upstreams)
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||||
@@ -300,7 +326,7 @@ class NativeCallSpec extends Specification {
|
|||||||
def upstreams = Mock(MultistreamHolder) {
|
def upstreams = Mock(MultistreamHolder) {
|
||||||
_ * it.observeChains() >> Flux.empty()
|
_ * it.observeChains() >> Flux.empty()
|
||||||
}
|
}
|
||||||
def nativeCall = new NativeCall(upstreams)
|
def nativeCall = nativeCall(upstreams)
|
||||||
|
|
||||||
def item = BlockchainOuterClass.NativeCallItem.newBuilder()
|
def item = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||||
.setId(1)
|
.setId(1)
|
||||||
@@ -338,7 +364,7 @@ class NativeCallSpec extends Specification {
|
|||||||
def multistreamHolder = Mock(MultistreamHolder) {
|
def multistreamHolder = Mock(MultistreamHolder) {
|
||||||
_ * it.observeChains() >> Flux.empty()
|
_ * it.observeChains() >> Flux.empty()
|
||||||
}
|
}
|
||||||
def nativeCall = new NativeCall(multistreamHolder)
|
def nativeCall = nativeCall(multistreamHolder)
|
||||||
|
|
||||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||||
@@ -365,8 +391,8 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Parse empty params"() {
|
def "Parse empty params"() {
|
||||||
setup:
|
setup:
|
||||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
def nativeCall = nativeCall()
|
||||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||||
new NativeCall.RawCallDetails("eth_test", "[]"))
|
new NativeCall.RawCallDetails("eth_test", "[]"))
|
||||||
when:
|
when:
|
||||||
def act = nativeCall.parseParams(ctx)
|
def act = nativeCall.parseParams(ctx)
|
||||||
@@ -378,8 +404,8 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Parse none params"() {
|
def "Parse none params"() {
|
||||||
setup:
|
setup:
|
||||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
def nativeCall = nativeCall()
|
||||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||||
new NativeCall.RawCallDetails("eth_test", ""))
|
new NativeCall.RawCallDetails("eth_test", ""))
|
||||||
when:
|
when:
|
||||||
def act = nativeCall.parseParams(ctx)
|
def act = nativeCall.parseParams(ctx)
|
||||||
@@ -391,8 +417,8 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Parse single param"() {
|
def "Parse single param"() {
|
||||||
setup:
|
setup:
|
||||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
def nativeCall = nativeCall()
|
||||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||||
new NativeCall.RawCallDetails("eth_test", "[false]"))
|
new NativeCall.RawCallDetails("eth_test", "[false]"))
|
||||||
when:
|
when:
|
||||||
def act = nativeCall.parseParams(ctx)
|
def act = nativeCall.parseParams(ctx)
|
||||||
@@ -404,8 +430,8 @@ class NativeCallSpec extends Specification {
|
|||||||
|
|
||||||
def "Parse multi param"() {
|
def "Parse multi param"() {
|
||||||
setup:
|
setup:
|
||||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
def nativeCall = nativeCall()
|
||||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||||
new NativeCall.RawCallDetails("eth_test", "[false, 123]"))
|
new NativeCall.RawCallDetails("eth_test", "[false, 123]"))
|
||||||
when:
|
when:
|
||||||
def act = nativeCall.parseParams(ctx)
|
def act = nativeCall.parseParams(ctx)
|
||||||
@@ -419,12 +445,11 @@ class NativeCallSpec extends Specification {
|
|||||||
//TODO
|
//TODO
|
||||||
def "Calls cache before remote"() {
|
def "Calls cache before remote"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(MultistreamHolder)
|
def nativeCall = nativeCall()
|
||||||
def nativeCall = new NativeCall(upstreams)
|
|
||||||
def api = TestingCommons.api()
|
def api = TestingCommons.api()
|
||||||
def upstream = TestingCommons.multistream(api)
|
def upstream = TestingCommons.multistream(api)
|
||||||
|
|
||||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10,
|
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10, null,
|
||||||
upstream,
|
upstream,
|
||||||
Selector.empty, new AlwaysQuorum(),
|
Selector.empty, new AlwaysQuorum(),
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
@@ -438,11 +463,10 @@ class NativeCallSpec extends Specification {
|
|||||||
//TODO
|
//TODO
|
||||||
def "Uses cached value"() {
|
def "Uses cached value"() {
|
||||||
setup:
|
setup:
|
||||||
def upstreams = Stub(MultistreamHolder)
|
def nativeCall = nativeCall()
|
||||||
def nativeCall = new NativeCall(upstreams)
|
|
||||||
def upstream = TestingCommons.multistream(TestingCommons.api())
|
def upstream = TestingCommons.multistream(TestingCommons.api())
|
||||||
|
|
||||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10,
|
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10, null,
|
||||||
upstream,
|
upstream,
|
||||||
Selector.empty, new AlwaysQuorum(),
|
Selector.empty, new AlwaysQuorum(),
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
.setBlock(
|
.setBlock(
|
||||||
Common.BlockInfo.newBuilder()
|
Common.BlockInfo.newBuilder()
|
||||||
.setHeight(blockJson.number)
|
.setHeight(blockJson.number)
|
||||||
.setWeight(ByteString.copyFrom(blockJson.totalDifficulty.toByteArray()))
|
|
||||||
.setBlockId(blockJson.hash.toHex().substring(2))
|
.setBlockId(blockJson.hash.toHex().substring(2))
|
||||||
.setTimestamp(blockJson.timestamp.toEpochMilli())
|
.setTimestamp(blockJson.timestamp.toEpochMilli())
|
||||||
).build()
|
).build()
|
||||||
@@ -280,7 +279,6 @@ class TrackEthereumTxSpec extends Specification {
|
|||||||
.setBlock(
|
.setBlock(
|
||||||
Common.BlockInfo.newBuilder()
|
Common.BlockInfo.newBuilder()
|
||||||
.setHeight(blocks[2].number)
|
.setHeight(blocks[2].number)
|
||||||
.setWeight(ByteString.copyFrom(blocks[2].totalDifficulty.toByteArray()))
|
|
||||||
.setBlockId(blocks[2].hash.toHex().substring(2))
|
.setBlockId(blocks[2].hash.toHex().substring(2))
|
||||||
.setTimestamp(blocks[2].timestamp.toEpochMilli())
|
.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")
|
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>
|
} as Callable<JsonRpcResponse>
|
||||||
return Mono.fromCallable(call)
|
return Mono.fromCallable(call)
|
||||||
}
|
}
|
||||||
@@ -323,7 +323,7 @@ class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class FilteredApisSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
List<Upstream> standard = (0..1).collect {
|
List<Upstream> standard = (0..1).collect {
|
||||||
TestingCommons.upstream(
|
TestingCommons.upstream(
|
||||||
it.toString(),
|
"test_" + it,
|
||||||
new EthereumApiStub(it)
|
new EthereumApiStub(it)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,7 @@ class FilteredApisSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
List<Upstream> standard = (0..1).collect {
|
List<Upstream> standard = (0..1).collect {
|
||||||
TestingCommons.upstream(
|
TestingCommons.upstream(
|
||||||
it.toString(),
|
"test_" + it,
|
||||||
new EthereumApiStub(it)
|
new EthereumApiStub(it)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ class MultistreamSpec extends Specification {
|
|||||||
|
|
||||||
def "Filter Best Status accepts better input"() {
|
def "Filter Best Status accepts better input"() {
|
||||||
setup:
|
setup:
|
||||||
def up1 = TestingCommons.upstream("1")
|
def up1 = TestingCommons.upstream("test-1")
|
||||||
def up2 = TestingCommons.upstream("2")
|
def up2 = TestingCommons.upstream("test-2")
|
||||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||||
def filter = new Multistream.FilterBestAvailability()
|
def filter = new Multistream.FilterBestAvailability()
|
||||||
def update0 = new Multistream.UpstreamStatus(
|
def update0 = new Multistream.UpstreamStatus(
|
||||||
@@ -87,8 +87,8 @@ class MultistreamSpec extends Specification {
|
|||||||
|
|
||||||
def "Filter Best Status declines worse input"() {
|
def "Filter Best Status declines worse input"() {
|
||||||
setup:
|
setup:
|
||||||
def up1 = TestingCommons.upstream("1")
|
def up1 = TestingCommons.upstream("test-1")
|
||||||
def up2 = TestingCommons.upstream("2")
|
def up2 = TestingCommons.upstream("test-2")
|
||||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||||
def filter = new Multistream.FilterBestAvailability()
|
def filter = new Multistream.FilterBestAvailability()
|
||||||
def update0 = new Multistream.UpstreamStatus(
|
def update0 = new Multistream.UpstreamStatus(
|
||||||
@@ -106,7 +106,7 @@ class MultistreamSpec extends Specification {
|
|||||||
|
|
||||||
def "Filter Best Status accepts worse input from same upstream"() {
|
def "Filter Best Status accepts worse input from same upstream"() {
|
||||||
setup:
|
setup:
|
||||||
def up = TestingCommons.upstream("1")
|
def up = TestingCommons.upstream("test-1")
|
||||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||||
def filter = new Multistream.FilterBestAvailability()
|
def filter = new Multistream.FilterBestAvailability()
|
||||||
def update0 = new Multistream.UpstreamStatus(
|
def update0 = new Multistream.UpstreamStatus(
|
||||||
@@ -124,8 +124,8 @@ class MultistreamSpec extends Specification {
|
|||||||
|
|
||||||
def "Filter Best Status accepts any input if existing is outdated"() {
|
def "Filter Best Status accepts any input if existing is outdated"() {
|
||||||
setup:
|
setup:
|
||||||
def up1 = TestingCommons.upstream("1")
|
def up1 = TestingCommons.upstream("test-1")
|
||||||
def up2 = TestingCommons.upstream("2")
|
def up2 = TestingCommons.upstream("test-2")
|
||||||
def time0 = Instant.now() - Duration.ofSeconds(90)
|
def time0 = Instant.now() - Duration.ofSeconds(90)
|
||||||
def filter = new Multistream.FilterBestAvailability()
|
def filter = new Multistream.FilterBestAvailability()
|
||||||
def update0 = new Multistream.UpstreamStatus(
|
def update0 = new Multistream.UpstreamStatus(
|
||||||
@@ -143,9 +143,9 @@ class MultistreamSpec extends Specification {
|
|||||||
|
|
||||||
def "Filter Best Status declines same status"() {
|
def "Filter Best Status declines same status"() {
|
||||||
setup:
|
setup:
|
||||||
def up1 = TestingCommons.upstream("1")
|
def up1 = TestingCommons.upstream("test-1")
|
||||||
def up2 = TestingCommons.upstream("2")
|
def up2 = TestingCommons.upstream("test-2")
|
||||||
def up3 = TestingCommons.upstream("3")
|
def up3 = TestingCommons.upstream("test-3")
|
||||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||||
def filter = new Multistream.FilterBestAvailability()
|
def filter = new Multistream.FilterBestAvailability()
|
||||||
def update0 = new Multistream.UpstreamStatus(
|
def update0 = new Multistream.UpstreamStatus(
|
||||||
@@ -172,7 +172,7 @@ class MultistreamSpec extends Specification {
|
|||||||
|
|
||||||
def "Call postprocess after api use"() {
|
def "Call postprocess after api use"() {
|
||||||
setup:
|
setup:
|
||||||
def request = new JsonRpcRequest("test_foo", [1], 1)
|
def request = new JsonRpcRequest("test_foo", [1], 1, null)
|
||||||
|
|
||||||
def api = TestingCommons.api()
|
def api = TestingCommons.api()
|
||||||
api.answer("test_foo", [1], "test")
|
api.answer("test_foo", [1], "test")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class RequestPostprocessorSpec extends Specification {
|
|||||||
|
|
||||||
def "Wrappers calls onReceive for a value"() {
|
def "Wrappers calls onReceive for a value"() {
|
||||||
setup:
|
setup:
|
||||||
def request = new JsonRpcRequest("test_foo", [1], 1)
|
def request = new JsonRpcRequest("test_foo", [1], 1, null)
|
||||||
def processor = Mock(RequestPostprocessor)
|
def processor = Mock(RequestPostprocessor)
|
||||||
def api = TestingCommons.api()
|
def api = TestingCommons.api()
|
||||||
api.answer("test_foo", [1], "test")
|
api.answer("test_foo", [1], "test")
|
||||||
@@ -30,7 +30,7 @@ class RequestPostprocessorSpec extends Specification {
|
|||||||
|
|
||||||
def "Wrappers doesn't call onReceive for no value"() {
|
def "Wrappers doesn't call onReceive for no value"() {
|
||||||
setup:
|
setup:
|
||||||
def request = new JsonRpcRequest("test_foo", [1], 1)
|
def request = new JsonRpcRequest("test_foo", [1], 1, null)
|
||||||
def processor = Mock(RequestPostprocessor)
|
def processor = Mock(RequestPostprocessor)
|
||||||
Reader<JsonRpcRequest, JsonRpcResponse> reader = Mock(Reader) {
|
Reader<JsonRpcRequest, JsonRpcResponse> reader = Mock(Reader) {
|
||||||
1 * it.read(request) >> Mono.empty()
|
1 * it.read(request) >> Mono.empty()
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ class EthereumDirectReaderSpec extends Specification {
|
|||||||
up, Caches.default(), new CurrentBlockCache(), calls
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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
|
up, Caches.default(), new CurrentBlockCache(), calls
|
||||||
)
|
)
|
||||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||||
1 * create(_, _) >> Mock(Reader) {
|
1 * create(_, _, _) >> Mock(Reader) {
|
||||||
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just(
|
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just(
|
||||||
new QuorumRpcReader.Result(
|
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"
|
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"() {
|
def "getBlockByNumber with latest uses latest id"() {
|
||||||
setup:
|
setup:
|
||||||
def head = Mock(Head) {
|
def head = Mock(Head) {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class WsConnectionSpec extends Specification {
|
|||||||
|
|
||||||
when:
|
when:
|
||||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
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:
|
then:
|
||||||
StepVerifier.create(act)
|
StepVerifier.create(act)
|
||||||
@@ -105,7 +105,7 @@ class WsConnectionSpec extends Specification {
|
|||||||
|
|
||||||
when:
|
when:
|
||||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
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:
|
then:
|
||||||
StepVerifier.create(act)
|
StepVerifier.create(act)
|
||||||
@@ -129,7 +129,7 @@ class WsConnectionSpec extends Specification {
|
|||||||
|
|
||||||
when:
|
when:
|
||||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
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:
|
then:
|
||||||
StepVerifier.create(act)
|
StepVerifier.create(act)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
when:
|
when:
|
||||||
def act = resp1.equals(resp2)
|
def act = resp1.equals(resp2)
|
||||||
then:
|
then:
|
||||||
act == true
|
act
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Extract processed string without quoted"() {
|
def "Extract processed string without quoted"() {
|
||||||
@@ -49,7 +49,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Fails to extract processed string if not quoted"() {
|
def "Fails to extract processed string if not quoted"() {
|
||||||
when:
|
when:
|
||||||
def act = new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString
|
new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString
|
||||||
then:
|
then:
|
||||||
thrown(IllegalStateException)
|
thrown(IllegalStateException)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize int id and null result"() {
|
def "Serialize int id and null result"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -72,7 +72,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize int id and string result"() {
|
def "Serialize int id and string result"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -81,7 +81,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize int id and object result"() {
|
def "Serialize int id and object result"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize int id and error"() {
|
def "Serialize int id and error"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -99,7 +99,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize string id and null result"() {
|
def "Serialize string id and null result"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -108,7 +108,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize string id and string result"() {
|
def "Serialize string id and string result"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -117,7 +117,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
|
|
||||||
def "Serialize string id and object result"() {
|
def "Serialize string id and object result"() {
|
||||||
setup:
|
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:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
then:
|
||||||
@@ -128,7 +128,7 @@ class JsonRpcResponseSpec extends Specification {
|
|||||||
setup:
|
setup:
|
||||||
def json = new JsonRpcResponse(null,
|
def json = new JsonRpcResponse(null,
|
||||||
new JsonRpcError(-32041, "Oooops"),
|
new JsonRpcError(-32041, "Oooops"),
|
||||||
new JsonRpcResponse.StringId("9kbo29gkaasf"))
|
new JsonRpcResponse.StringId("9kbo29gkaasf"), null)
|
||||||
when:
|
when:
|
||||||
def act = objectMapper.writeValueAsString(json)
|
def act = objectMapper.writeValueAsString(json)
|
||||||
then:
|
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:
|
redis:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
signature:
|
||||||
|
enabled: true
|
||||||
|
algorithm: ECDSA
|
||||||
|
signatureScheme: SHA256withECDSA
|
||||||
|
privateKey: "./test_key"
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
port: 18080
|
port: 18080
|
||||||
tls:
|
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 {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url "https://maven.emrld.io" }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -14,12 +15,25 @@ dependencies {
|
|||||||
implementation "org.codehaus.groovy:groovy:3.0.4"
|
implementation "org.codehaus.groovy:groovy:3.0.4"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-core:2.9.8"
|
implementation "com.fasterxml.jackson.core:jackson-core:2.9.8"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind: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"
|
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 {
|
test {
|
||||||
systemProperty "trialMode", project.getProperty("dshackleTrialMode")
|
systemProperty "trialMode", project.getProperty("dshackleTrialMode")
|
||||||
|
systemProperty "signatureKey", project.getProperty("signatureKey")
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
testLogging {
|
testLogging {
|
||||||
events "PASSED", "FAILED"
|
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.HttpClientBuilder
|
||||||
import org.apache.http.impl.client.HttpClients
|
import org.apache.http.impl.client.HttpClients
|
||||||
|
|
||||||
class ProxyClient {
|
class ProxyClient implements Client {
|
||||||
|
|
||||||
private int sequence = 0;
|
private int sequence = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
package io.emeraldpay.dshackle.testing.trial.basicproxy
|
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 io.emeraldpay.dshackle.testing.trial.ProxyClient
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import spock.lang.IgnoreIf
|
import spock.lang.IgnoreIf
|
||||||
|
import spock.lang.Shared
|
||||||
import spock.lang.Specification
|
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' })
|
@IgnoreIf({ System.getProperty('trialMode') != 'basic' })
|
||||||
class StandardCallsSpec extends Specification {
|
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"() {
|
def "get height"() {
|
||||||
when:
|
when:
|
||||||
@@ -15,6 +30,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act.result == "0x100001"
|
act.result == "0x100001"
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "get block"() {
|
def "get block"() {
|
||||||
@@ -31,6 +48,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "get non-existing block"() {
|
def "get non-existing block"() {
|
||||||
@@ -39,6 +58,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act.result == null
|
act.result == null
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "get tx"() {
|
def "get tx"() {
|
||||||
@@ -50,6 +71,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
blockHash == "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e"
|
blockHash == "0x9a834c53bbee9c2665a5a84789a1d1ad73750b2d77b50de44f457f411d02e52e"
|
||||||
}
|
}
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "get non-existing tx"() {
|
def "get non-existing tx"() {
|
||||||
@@ -58,6 +81,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
act.result == null
|
act.result == null
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "get block with txes"() {
|
def "get block with txes"() {
|
||||||
@@ -76,6 +101,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "returns original block json"() {
|
def "returns original block json"() {
|
||||||
@@ -87,6 +114,8 @@ class StandardCallsSpec extends Specification {
|
|||||||
testFoo == "bar"
|
testFoo == "bar"
|
||||||
}
|
}
|
||||||
act.error == null
|
act.error == null
|
||||||
|
where:
|
||||||
|
client << clients
|
||||||
}
|
}
|
||||||
|
|
||||||
def "returns original block json with tx"() {
|
def "returns original block json with tx"() {
|
||||||
@@ -98,5 +127,33 @@ class StandardCallsSpec extends Specification {
|
|||||||
testFoo == "bar"
|
testFoo == "bar"
|
||||||
}
|
}
|
||||||
act.error == null
|
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