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:
co-authored by
Igor Artamonov
parent
530811d272
commit
d1ad77a345
@@ -21,8 +21,10 @@ import io.emeraldpay.dshackle.config.HealthConfig
|
||||
import io.emeraldpay.dshackle.config.MainConfig
|
||||
import io.emeraldpay.dshackle.config.MainConfigReader
|
||||
import io.emeraldpay.dshackle.config.MonitoringConfig
|
||||
import io.emeraldpay.dshackle.config.SignatureConfig
|
||||
import io.emeraldpay.dshackle.config.TokensConfig
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
@@ -37,6 +39,7 @@ import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import reactor.core.scheduler.Scheduler
|
||||
import reactor.core.scheduler.Schedulers
|
||||
import java.io.File
|
||||
import java.security.Security
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@@ -66,6 +69,8 @@ open class Config(
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
fun getConfigPath(): File {
|
||||
@@ -119,6 +124,11 @@ open class Config(
|
||||
return mainConfig.cache ?: CacheConfig()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun signatureConfig(@Autowired mainConfig: MainConfig): SignatureConfig {
|
||||
return mainConfig.signature ?: SignatureConfig()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun tokensConfig(@Autowired mainConfig: MainConfig): TokensConfig {
|
||||
return mainConfig.tokens ?: TokensConfig(emptyList())
|
||||
|
||||
@@ -26,4 +26,5 @@ class MainConfig {
|
||||
var monitoring: MonitoringConfig = MonitoringConfig.default()
|
||||
var accessLogConfig: AccessLogConfig = AccessLogConfig.default()
|
||||
var health: HealthConfig = HealthConfig.default()
|
||||
var signature: SignatureConfig? = null
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class MainConfigReader(
|
||||
private val monitoringConfigReader = MonitoringConfigReader()
|
||||
private val accessLogReader = AccessLogReader()
|
||||
private val healthConfigReader = HealthConfigReader()
|
||||
private val signatureConfigReader = SignatureConfigReader(fileResolver)
|
||||
|
||||
fun read(input: InputStream): MainConfig? {
|
||||
val configNode = readNode(input)
|
||||
@@ -75,6 +76,9 @@ class MainConfigReader(
|
||||
healthConfigReader.read(input).let {
|
||||
config.health = it
|
||||
}
|
||||
signatureConfigReader.read(input).let {
|
||||
config.signature = it
|
||||
}
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
class SignatureConfig {
|
||||
|
||||
enum class Algorithm {
|
||||
SECP256K1
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun algorithmOfString(algo: String): Algorithm {
|
||||
val algorithm = when (algo.uppercase(Locale.getDefault())) {
|
||||
"SECP256K1" -> Algorithm.SECP256K1
|
||||
else -> throw IllegalArgumentException("Unknown algorithm or not allowed")
|
||||
}
|
||||
return algorithm
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signature scheme that we should use
|
||||
*/
|
||||
var algorithm: Algorithm = Algorithm.SECP256K1
|
||||
/**
|
||||
* Should we generate signature on this instance if it's not already present
|
||||
*/
|
||||
var enabled: Boolean = false
|
||||
|
||||
var privateKey: String? = null
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import io.emeraldpay.dshackle.FileResolver
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import java.io.InputStream
|
||||
|
||||
class SignatureConfigReader(val fileResolver: FileResolver) : YamlConfigReader(), ConfigReader<SignatureConfig> {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SignatureConfig::class.java)
|
||||
}
|
||||
|
||||
fun read(input: InputStream): SignatureConfig? {
|
||||
val configNode = readNode(input)
|
||||
return read(configNode)
|
||||
}
|
||||
|
||||
override fun read(input: MappingNode?): SignatureConfig? {
|
||||
return getMapping(input, "signed-response")?.let { node ->
|
||||
val config = SignatureConfig()
|
||||
getValueAsBool(node, "enabled")?.let {
|
||||
config.enabled = it
|
||||
}
|
||||
if (config.enabled) {
|
||||
getValueAsString(node, "algorithm")?.let {
|
||||
config.algorithm = SignatureConfig.algorithmOfString(it)
|
||||
}
|
||||
getValueAsString(node, "private-key")?.let {
|
||||
val key = fileResolver.resolve(it)
|
||||
config.privateKey = key.absolutePath
|
||||
}
|
||||
}
|
||||
if (config.enabled && config.privateKey == null) {
|
||||
throw IllegalStateException("Path to a private key (`signature.private-key`) is required when Response signature is enabled.")
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,8 @@ class UpstreamsConfigReader(
|
||||
|
||||
fun isValid(upstream: UpstreamsConfig.Upstream<*>): Boolean {
|
||||
val id = upstream.id
|
||||
// In general, we just check that id is suitable for urls and references,
|
||||
// Besides that, if Response Signatures are enabled (CurrentResponseSigner) then it's critical that the id cannot have `/` symbol
|
||||
if (id == null || id.length < 3 || !id.matches(Regex("[a-zA-Z][a-zA-Z0-9_-]+[a-zA-Z0-9]"))) {
|
||||
log.warn("Invalid id: $id")
|
||||
return false
|
||||
|
||||
@@ -110,7 +110,9 @@ class Events {
|
||||
val payloadSizeBytes: Long,
|
||||
val nativeCall: NativeCallItemDetails,
|
||||
val responseBody: String? = null,
|
||||
val errorMessage: String? = null
|
||||
val errorMessage: String? = null,
|
||||
val nonce: Long? = null,
|
||||
val signature: String? = null
|
||||
) : ChainBase(blockchain, "NativeCall", id, channel)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@@ -163,6 +165,7 @@ class Events {
|
||||
val method: String,
|
||||
val id: Int,
|
||||
val payloadSizeBytes: Long,
|
||||
val nonce: Long,
|
||||
val requestParams: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.grpc.Attributes
|
||||
import io.grpc.Grpc
|
||||
import io.grpc.Metadata
|
||||
import io.netty.handler.codec.http.HttpHeaders
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.netty.http.server.HttpServerRequest
|
||||
@@ -312,6 +313,7 @@ class EventsBuilder {
|
||||
item.method,
|
||||
item.id,
|
||||
item.payload.size().toLong(),
|
||||
item.nonce,
|
||||
if (accessLogConfig.includeMessages) {
|
||||
if (item.payload != null && !item.payload.isEmpty && item.payload.isValidUtf8) item.payload.toStringUtf8() else ""
|
||||
} else null
|
||||
@@ -336,6 +338,8 @@ class EventsBuilder {
|
||||
if (msg.payload != null && !msg.payload.isEmpty && msg.payload.isValidUtf8) msg.payload.toStringUtf8() else ""
|
||||
} else null,
|
||||
errorMessage = if (accessLogConfig.includeMessages) msg.errorMessage else null,
|
||||
signature = Hex.encodeHexString(msg.signature.signature.toByteArray()),
|
||||
nonce = msg.signature.nonce
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ abstract class BaseHandler(
|
||||
// If Proxy is configured to preserve original order it means that a client expect responses at exact same position
|
||||
// as requests even if a request completely failed for a some reason. It's very unlikely situation, but still possible
|
||||
// At this case, if we found a gap in responses, we put a default response with an error
|
||||
?: NativeCall.CallResult(id, null, NativeCall.CallError(id, "No response", null))
|
||||
?: NativeCall.CallResult(id, null, null, NativeCall.CallError(id, "No response", null), null)
|
||||
}
|
||||
}
|
||||
.flatMapMany {
|
||||
|
||||
@@ -180,7 +180,7 @@ class WebsocketHandler(
|
||||
}
|
||||
Mono.just(response)
|
||||
.map { Global.objectMapper.writeValueAsString(it) }
|
||||
.doOnNext { eventHandler.onResponse(NativeCall.CallResult.ok(0, it.toByteArray())) }
|
||||
.doOnNext { eventHandler.onResponse(NativeCall.CallResult.ok(0, null, it.toByteArray(), null)) }
|
||||
.doFinally { eventHandler.close() }
|
||||
} else {
|
||||
val eventHandler: AccessHandlerHttp.RequestHandler = eventHandlerFactory.call()
|
||||
|
||||
@@ -20,12 +20,14 @@ import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
|
||||
open class AlwaysQuorum : CallQuorum {
|
||||
|
||||
private var resolved = false
|
||||
private var result: ByteArray? = null
|
||||
private var rpcError: JsonRpcError? = null
|
||||
private var sig: ResponseSigner.Signature? = null
|
||||
|
||||
override fun init(head: Head) {
|
||||
}
|
||||
@@ -38,14 +40,20 @@ open class AlwaysQuorum : CallQuorum {
|
||||
return rpcError != null
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||
override fun getSignature(): ResponseSigner.Signature? {
|
||||
return sig
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean {
|
||||
result = response
|
||||
resolved = true
|
||||
sig = signature
|
||||
return true
|
||||
}
|
||||
|
||||
override fun record(error: JsonRpcException, upstream: Upstream) {
|
||||
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
this.rpcError = error.error
|
||||
sig = signature
|
||||
}
|
||||
|
||||
override fun getResult(): ByteArray? {
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
|
||||
open class BroadcastQuorum(
|
||||
val quorum: Int = 3
|
||||
@@ -26,6 +27,7 @@ open class BroadcastQuorum(
|
||||
private var result: ByteArray? = null
|
||||
private var txid: String? = null
|
||||
private var calls = 0
|
||||
private var sig: ResponseSigner.Signature? = null
|
||||
|
||||
override fun init(head: Head) {
|
||||
}
|
||||
@@ -42,19 +44,25 @@ open class BroadcastQuorum(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
||||
override fun getSignature(): ResponseSigner.Signature? {
|
||||
return sig
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
calls++
|
||||
if (txid == null && responseValue != null) {
|
||||
txid = responseValue
|
||||
sig = signature
|
||||
result = response
|
||||
}
|
||||
}
|
||||
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
// can be "message: known transaction: TXID", "Transaction with the same hash was already imported" or "message: Nonce too low"
|
||||
calls++
|
||||
if (result == null) {
|
||||
result = response
|
||||
sig = signature
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,7 @@ import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import reactor.util.function.Tuple2
|
||||
import java.util.function.BiFunction
|
||||
import java.util.function.Predicate
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
|
||||
interface CallQuorum {
|
||||
|
||||
@@ -31,23 +29,9 @@ interface CallQuorum {
|
||||
fun isResolved(): Boolean
|
||||
fun isFailed(): Boolean
|
||||
|
||||
fun record(response: ByteArray, upstream: Upstream): Boolean
|
||||
fun record(error: JsonRpcException, upstream: Upstream)
|
||||
fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean
|
||||
fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream)
|
||||
fun getSignature(): ResponseSigner.Signature?
|
||||
fun getResult(): ByteArray?
|
||||
fun getError(): JsonRpcError?
|
||||
|
||||
companion object {
|
||||
fun untilResolved(cq: CallQuorum): Predicate<Any> {
|
||||
return Predicate { _ ->
|
||||
!cq.isResolved()
|
||||
}
|
||||
}
|
||||
|
||||
fun asReducer(): BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> {
|
||||
return BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { a, b ->
|
||||
a.record(b.t1, b.t2)
|
||||
return@BiFunction a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
|
||||
open class NonEmptyQuorum(
|
||||
val maxTries: Int = 3
|
||||
@@ -25,7 +26,7 @@ open class NonEmptyQuorum(
|
||||
|
||||
private var result: ByteArray? = null
|
||||
private var tries: Int = 0
|
||||
|
||||
private var sig: ResponseSigner.Signature? = null
|
||||
override fun init(head: Head) {
|
||||
}
|
||||
|
||||
@@ -37,10 +38,15 @@ open class NonEmptyQuorum(
|
||||
return tries >= maxTries
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: Any?, upstream: Upstream) {
|
||||
override fun getSignature(): ResponseSigner.Signature? {
|
||||
return sig
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: Any?, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
tries++
|
||||
if (responseValue != null) {
|
||||
result = response
|
||||
sig = signature
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +54,7 @@ open class NonEmptyQuorum(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, sig: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
tries++
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.hex.HexQuantity
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
@@ -31,6 +32,7 @@ open class NonceQuorum(
|
||||
private var result: ByteArray? = null
|
||||
private var receivedTimes = 0
|
||||
private var errors = 0
|
||||
private var sig: ResponseSigner.Signature? = null
|
||||
|
||||
override fun init(head: Head) {
|
||||
}
|
||||
@@ -44,8 +46,11 @@ open class NonceQuorum(
|
||||
override fun isFailed(): Boolean {
|
||||
return errors >= tries
|
||||
}
|
||||
override fun getSignature(): ResponseSigner.Signature? {
|
||||
return sig
|
||||
}
|
||||
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, upstream: Upstream) {
|
||||
override fun recordValue(response: ByteArray, responseValue: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
val value = responseValue?.let { str ->
|
||||
HexQuantity.from(str).value.toLong()
|
||||
}
|
||||
@@ -54,8 +59,10 @@ open class NonceQuorum(
|
||||
if (value != null && value > resultValue) {
|
||||
resultValue = value
|
||||
result = response
|
||||
sig = signature
|
||||
} else if (result == null) {
|
||||
result = response
|
||||
sig = signature
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +71,7 @@ open class NonceQuorum(
|
||||
return result
|
||||
}
|
||||
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream) {
|
||||
override fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
errors++
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,7 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
|
||||
private val result: AtomicReference<ByteArray> = AtomicReference()
|
||||
private val failed = AtomicReference(false)
|
||||
private var rpcError: JsonRpcError? = null
|
||||
private var sig: ResponseSigner.Signature? = null
|
||||
|
||||
override fun init(head: Head) {
|
||||
}
|
||||
@@ -44,16 +46,17 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
|
||||
return failed.get()
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||
override fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean {
|
||||
val lagging = upstream.getLag() > maxLag
|
||||
if (!lagging) {
|
||||
result.set(response)
|
||||
sig = signature
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun record(error: JsonRpcException, upstream: Upstream) {
|
||||
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
this.rpcError = error.error
|
||||
val lagging = upstream.getLag() > maxLag
|
||||
if (!lagging && result.get() == null) {
|
||||
@@ -61,6 +64,9 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSignature(): ResponseSigner.Signature? {
|
||||
return sig
|
||||
}
|
||||
override fun getResult(): ByteArray {
|
||||
return result.get()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.ApiSource
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
|
||||
// creates instance of a Quorum based reader
|
||||
interface QuorumReaderFactory {
|
||||
@@ -28,11 +29,11 @@ interface QuorumReaderFactory {
|
||||
}
|
||||
}
|
||||
|
||||
fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result>
|
||||
fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?): Reader<JsonRpcRequest, QuorumRpcReader.Result>
|
||||
|
||||
class Default : QuorumReaderFactory {
|
||||
override fun create(apis: ApiSource, quorum: CallQuorum): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||
return QuorumRpcReader(apis, quorum)
|
||||
override fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?): Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||
return QuorumRpcReader(apis, quorum, signer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,15 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.util.function.Tuple2
|
||||
import reactor.util.function.Tuple3
|
||||
import reactor.util.function.Tuples
|
||||
import java.util.Optional
|
||||
import java.util.function.BiFunction
|
||||
import java.util.function.Function
|
||||
|
||||
@@ -36,13 +39,16 @@ import java.util.function.Function
|
||||
*/
|
||||
class QuorumRpcReader(
|
||||
private val apiControl: ApiSource,
|
||||
private val quorum: CallQuorum
|
||||
private val quorum: CallQuorum,
|
||||
private val signer: ResponseSigner?,
|
||||
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
|
||||
}
|
||||
|
||||
constructor(apiControl: ApiSource, quorum: CallQuorum) : this(apiControl, quorum, null)
|
||||
|
||||
override fun read(key: JsonRpcRequest): Mono<Result> {
|
||||
// needs at least one response, so start a request
|
||||
apiControl.request(1)
|
||||
@@ -83,8 +89,8 @@ class QuorumRpcReader(
|
||||
}
|
||||
|
||||
fun execute(key: JsonRpcRequest, retrySpec: reactor.util.retry.Retry): Function<Flux<Upstream>, Mono<CallQuorum>> {
|
||||
val quorumReduce = BiFunction<CallQuorum, Tuple2<ByteArray, Upstream>, CallQuorum> { res, a ->
|
||||
if (res.record(a.t1, a.t2)) {
|
||||
val quorumReduce = BiFunction<CallQuorum, Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>, CallQuorum> { res, a ->
|
||||
if (res.record(a.t1, a.t2.orElse(null), a.t3)) {
|
||||
apiControl.resolve()
|
||||
} else {
|
||||
// quorum needs more responses, so ask api controller to make another
|
||||
@@ -112,38 +118,61 @@ class QuorumRpcReader(
|
||||
.filter { it.isResolved() } // return nothing if not resolved
|
||||
.map {
|
||||
// TODO find actual quorum number
|
||||
QuorumRpcReader.Result(it.getResult()!!, 1)
|
||||
QuorumRpcReader.Result(it.getResult()!!, it.getSignature(), 1)
|
||||
}
|
||||
.switchIfEmpty(defaultResult)
|
||||
}
|
||||
}
|
||||
|
||||
fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple2<ByteArray, Upstream>> {
|
||||
fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>> {
|
||||
return api.getApi()
|
||||
.read(key)
|
||||
.flatMap { response ->
|
||||
response.requireResult()
|
||||
.onErrorResume { err ->
|
||||
// on error notify quorum, it may use error message or other details
|
||||
val cleanErr: JsonRpcException = when (err) {
|
||||
is RpcException -> JsonRpcException.from(err)
|
||||
is JsonRpcException -> err
|
||||
else -> JsonRpcException(
|
||||
JsonRpcResponse.NumberId(key.id),
|
||||
JsonRpcError(-32603, "Unhandled internal error: ${err.javaClass}")
|
||||
)
|
||||
}
|
||||
quorum.record(cleanErr, api)
|
||||
// if it's failed after that, then we don't need more calls, stop api source
|
||||
if (quorum.isFailed()) {
|
||||
apiControl.resolve()
|
||||
} else {
|
||||
apiControl.request(1)
|
||||
}
|
||||
Mono.empty()
|
||||
}
|
||||
.transform(withSignature(api, key, response))
|
||||
.transform(withErrorResume(api, key))
|
||||
}
|
||||
.map { Tuples.of(it, api) }
|
||||
.map { Tuples.of(it.t1, it.t2, api) }
|
||||
}
|
||||
|
||||
fun withSignature(api: Upstream, key: JsonRpcRequest, response: JsonRpcResponse): Function<Mono<ByteArray>, Mono<Tuple2<ByteArray, Optional<ResponseSigner.Signature>>>> {
|
||||
return Function { src ->
|
||||
src.map {
|
||||
val signature = response.providedSignature
|
||||
?: if (key.nonce != null) {
|
||||
signer?.sign(key.nonce, response.getResult(), api)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Tuples.of(it, Optional.ofNullable(signature))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> withErrorResume(api: Upstream, key: JsonRpcRequest): Function<Mono<T>, Mono<T>> {
|
||||
return Function { src ->
|
||||
src.onErrorResume { err ->
|
||||
// when the call failed with an error we want to notify the quorum because
|
||||
// it may use the error message or other details
|
||||
//
|
||||
val cleanErr: JsonRpcException = when (err) {
|
||||
is RpcException -> JsonRpcException.from(err)
|
||||
is JsonRpcException -> err
|
||||
else -> JsonRpcException(
|
||||
JsonRpcResponse.NumberId(key.id),
|
||||
JsonRpcError(-32603, "Unhandled internal error: ${err.javaClass}")
|
||||
)
|
||||
}
|
||||
quorum.record(cleanErr, null, api)
|
||||
// if it's failed after that, then we don't need more calls, stop api source
|
||||
if (quorum.isFailed()) {
|
||||
apiControl.resolve()
|
||||
} else {
|
||||
apiControl.request(1)
|
||||
}
|
||||
Mono.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setupDefaultResult(key: JsonRpcRequest): Mono<Result> {
|
||||
@@ -162,6 +191,7 @@ class QuorumRpcReader(
|
||||
|
||||
class Result(
|
||||
val value: ByteArray,
|
||||
val signature: ResponseSigner.Signature?,
|
||||
val quorum: Int
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
@@ -34,26 +35,26 @@ abstract class ValueAwareQuorum<T>(
|
||||
return Global.objectMapper.readValue(response.inputStream(), clazz)
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||
override fun record(response: ByteArray, signature: ResponseSigner.Signature?, upstream: Upstream): Boolean {
|
||||
try {
|
||||
val value = extractValue(response, clazz)
|
||||
recordValue(response, value, upstream)
|
||||
recordValue(response, value, signature, upstream)
|
||||
} catch (e: RpcException) {
|
||||
recordError(response, e.rpcMessage, upstream)
|
||||
recordError(response, e.rpcMessage, signature, upstream)
|
||||
} catch (e: Exception) {
|
||||
recordError(response, e.message, upstream)
|
||||
recordError(response, e.message, signature, upstream)
|
||||
}
|
||||
return isResolved()
|
||||
}
|
||||
|
||||
override fun record(error: JsonRpcException, upstream: Upstream) {
|
||||
override fun record(error: JsonRpcException, signature: ResponseSigner.Signature?, upstream: Upstream) {
|
||||
this.rpcError = error.error
|
||||
recordError(null, error.error.message, upstream)
|
||||
recordError(null, error.error.message, signature, upstream)
|
||||
}
|
||||
|
||||
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
|
||||
abstract fun recordValue(response: ByteArray, responseValue: T?, signature: ResponseSigner.Signature?, upstream: Upstream)
|
||||
|
||||
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
|
||||
abstract fun recordError(response: ByteArray?, errorMessage: String?, signature: ResponseSigner.Signature?, upstream: Upstream)
|
||||
|
||||
override fun getError(): JsonRpcError? {
|
||||
return rpcError
|
||||
|
||||
@@ -44,7 +44,7 @@ class BlockchainRpc(
|
||||
@Autowired private val trackAddress: List<TrackAddress>,
|
||||
@Autowired private val describe: Describe,
|
||||
@Autowired private val subscribeStatus: SubscribeStatus,
|
||||
@Autowired private val estimateFee: EstimateFee
|
||||
@Autowired private val estimateFee: EstimateFee,
|
||||
) : ReactorBlockchainGrpc.BlockchainImplBase() {
|
||||
|
||||
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
|
||||
|
||||
@@ -34,6 +34,7 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.emeraldpay.grpc.BlockchainType
|
||||
@@ -49,7 +50,8 @@ import java.util.EnumMap
|
||||
|
||||
@Service
|
||||
open class NativeCall(
|
||||
@Autowired private val multistreamHolder: MultistreamHolder
|
||||
@Autowired private val multistreamHolder: MultistreamHolder,
|
||||
@Autowired private val signer: ResponseSigner,
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(NativeCall::class.java)
|
||||
@@ -85,7 +87,7 @@ open class NativeCall(
|
||||
} else {
|
||||
val error = it.getError()
|
||||
Mono.just(
|
||||
CallResult(error.id, null, error)
|
||||
CallResult(error.id, 0, null, error, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,10 +109,21 @@ open class NativeCall(
|
||||
} else {
|
||||
result.payload = ByteString.copyFrom(it.result)
|
||||
}
|
||||
|
||||
if (it.nonce != null && it.signature != null) {
|
||||
result.signature = buildSignature(it.nonce, it.signature)
|
||||
}
|
||||
return result.build()
|
||||
}
|
||||
|
||||
fun buildSignature(nonce: Long, signature: ResponseSigner.Signature): BlockchainOuterClass.NativeCallReplySignature {
|
||||
val msg = BlockchainOuterClass.NativeCallReplySignature.newBuilder()
|
||||
msg.signature = ByteString.copyFrom(signature.value)
|
||||
msg.keyId = signature.keyId
|
||||
msg.upstreamId = signature.upstreamId
|
||||
msg.nonce = nonce
|
||||
return msg.build()
|
||||
}
|
||||
|
||||
fun processException(it: Throwable?): Mono<BlockchainOuterClass.NativeCallReplyItem> {
|
||||
val id: Int = if (it != null && it is CallFailure) {
|
||||
it.id
|
||||
@@ -171,7 +184,6 @@ open class NativeCall(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// for ethereum the actual block needed for the call may be specified in the call parameters
|
||||
val callSpecificMatcher: Mono<Selector.Matcher> =
|
||||
if (BlockchainType.from(upstream.chain) == BlockchainType.ETHEREUM) {
|
||||
@@ -179,7 +191,6 @@ open class NativeCall(
|
||||
} else {
|
||||
null
|
||||
} ?: Mono.empty()
|
||||
|
||||
return callSpecificMatcher.defaultIfEmpty(Selector.empty).map { csm ->
|
||||
val matcher = Selector.Builder()
|
||||
.withMatcher(csm)
|
||||
@@ -196,27 +207,27 @@ open class NativeCall(
|
||||
val heightMatcher = Selector.HeightMatcher(minHeight)
|
||||
matcher.withMatcher(heightMatcher)
|
||||
}
|
||||
|
||||
ValidCallContext(requestItem.id, upstream, matcher.build(), callQuorum, RawCallDetails(method, params))
|
||||
val nonce = requestItem.nonce.let { if (it == 0L) null else it }
|
||||
ValidCallContext(requestItem.id, nonce, upstream, matcher.build(), callQuorum, RawCallDetails(method, params))
|
||||
}
|
||||
}
|
||||
|
||||
fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> {
|
||||
return ctx.upstream.getRoutedApi(ctx.matcher)
|
||||
.flatMap { api ->
|
||||
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
||||
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce))
|
||||
.flatMap(JsonRpcResponse::requireResult)
|
||||
.map {
|
||||
CallResult.ok(ctx.id, it)
|
||||
CallResult.ok(ctx.id, ctx.nonce, it, null)
|
||||
}
|
||||
}.switchIfEmpty(
|
||||
Mono.just(ctx).flatMap(this::executeOnRemote)
|
||||
)
|
||||
.onErrorResume {
|
||||
if (it is CallFailure) {
|
||||
Mono.just(CallResult.fail(it.id, it.reason))
|
||||
Mono.just(CallResult.fail(it.id, ctx.nonce, it.reason))
|
||||
} else {
|
||||
Mono.just(CallResult.fail(ctx.id, it))
|
||||
Mono.just(CallResult.fail(ctx.id, ctx.nonce, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,22 +237,22 @@ open class NativeCall(
|
||||
if (!ctx.upstream.getMethods().isCallable(ctx.payload.method)) {
|
||||
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
|
||||
}
|
||||
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum)
|
||||
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum, signer)
|
||||
return reader
|
||||
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params))
|
||||
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce))
|
||||
.map {
|
||||
CallResult(ctx.id, it.value, null)
|
||||
CallResult(ctx.id, ctx.nonce, it.value, null, it.signature)
|
||||
}
|
||||
.onErrorResume { t ->
|
||||
val failure = when (t) {
|
||||
is CallFailure -> CallResult.fail(t.id, t.reason)
|
||||
is JsonRpcException -> CallResult.fail(ctx.id, t.error.code, t.error.message)
|
||||
else -> CallResult.fail(ctx.id, t)
|
||||
is CallFailure -> CallResult.fail(t.id, ctx.nonce, t.reason)
|
||||
is JsonRpcException -> CallResult.fail(ctx.id, ctx.nonce, t.error.code, t.error.message)
|
||||
else -> CallResult.fail(ctx.id, ctx.nonce, t)
|
||||
}
|
||||
Mono.just(failure)
|
||||
}
|
||||
.switchIfEmpty(
|
||||
Mono.just(CallResult.fail(ctx.id, 1, "No response or no available upstream for ${ctx.payload.method}"))
|
||||
Mono.just(CallResult.fail(ctx.id, ctx.nonce, 1, "No response or no available upstream for ${ctx.payload.method}"))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -262,6 +273,7 @@ open class NativeCall(
|
||||
|
||||
open class ValidCallContext<T>(
|
||||
val id: Int,
|
||||
val nonce: Long?,
|
||||
val upstream: Multistream,
|
||||
val matcher: Selector.Matcher,
|
||||
val callQuorum: CallQuorum,
|
||||
@@ -280,7 +292,7 @@ open class NativeCall(
|
||||
}
|
||||
|
||||
fun <X> withPayload(payload: X): ValidCallContext<X> {
|
||||
return ValidCallContext(id, upstream, matcher, callQuorum, payload)
|
||||
return ValidCallContext(id, nonce, upstream, matcher, callQuorum, payload)
|
||||
}
|
||||
|
||||
fun getApis(): ApiSource {
|
||||
@@ -322,18 +334,18 @@ open class NativeCall(
|
||||
}
|
||||
}
|
||||
|
||||
open class CallResult(val id: Int, val result: ByteArray?, val error: CallError?) {
|
||||
open class CallResult(val id: Int, val nonce: Long?, val result: ByteArray?, val error: CallError?, val signature: ResponseSigner.Signature?) {
|
||||
companion object {
|
||||
fun ok(id: Int, result: ByteArray): CallResult {
|
||||
return CallResult(id, result, null)
|
||||
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?): CallResult {
|
||||
return CallResult(id, nonce, result, null, signature)
|
||||
}
|
||||
|
||||
fun fail(id: Int, errorCore: Int, errorMessage: String): CallResult {
|
||||
return CallResult(id, null, CallError(errorCore, errorMessage, null))
|
||||
fun fail(id: Int, nonce: Long?, errorCore: Int, errorMessage: String): CallResult {
|
||||
return CallResult(id, nonce, null, CallError(errorCore, errorMessage, null), null)
|
||||
}
|
||||
|
||||
fun fail(id: Int, error: Throwable): CallResult {
|
||||
return CallResult(id, null, CallError.from(error))
|
||||
fun fail(id: Int, nonce: Long?, error: Throwable): CallResult {
|
||||
return CallResult(id, nonce, null, CallError.from(error), null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.rpc
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
@@ -155,7 +154,6 @@ class TrackBitcoinTx(
|
||||
Common.BlockInfo.newBuilder()
|
||||
.setBlockId(tx.blockHash!!.substring(2))
|
||||
.setTimestamp(tx.blockTime!!.toEpochMilli())
|
||||
.setWeight(ByteString.copyFrom(tx.blockTotalDifficulty!!.toByteArray()))
|
||||
.setHeight(tx.height!!)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.rpc
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.SilentException
|
||||
@@ -258,7 +257,6 @@ class TrackEthereumTx(
|
||||
Common.BlockInfo.newBuilder()
|
||||
.setBlockId(tx.status.blockHash!!.toHex().substring(2))
|
||||
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
|
||||
.setWeight(ByteString.copyFrom(tx.status.blockTotalDifficulty!!.toByteArray()))
|
||||
.setHeight(tx.status.height!!)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ abstract class DefaultUpstream(
|
||||
.multicast()
|
||||
.directBestEffort<UpstreamAvailability>()
|
||||
|
||||
init {
|
||||
if (id.length < 3 || !id.matches(Regex("[a-zA-Z][a-zA-Z0-9_-]+[a-zA-Z0-9]"))) {
|
||||
throw IllegalArgumentException("Invalid upstream id: $id")
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return getStatus() == UpstreamAvailability.OK
|
||||
}
|
||||
|
||||
@@ -132,7 +132,8 @@ class EthereumDirectReader(
|
||||
*/
|
||||
private fun readWithQuorum(request: JsonRpcRequest): Mono<ByteArray> {
|
||||
return quorumReaderFactory
|
||||
.create(up.getApiSource(Selector.empty), callMethodsFactory.create().getQuorumFor(request.method))
|
||||
// we do not use Signer for internal requests because it doesn't make much sense
|
||||
.create(up.getApiSource(Selector.empty), callMethodsFactory.create().getQuorumFor(request.method), null)
|
||||
.read(request)
|
||||
.map { it.value }
|
||||
}
|
||||
|
||||
@@ -56,9 +56,14 @@ class LocalCallRouter(
|
||||
return Mono.just(methods.executeHardcoded(key.method))
|
||||
.map { JsonRpcResponse(it, null) }
|
||||
}
|
||||
|
||||
if (!methods.isCallable(key.method)) {
|
||||
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
|
||||
}
|
||||
if (key.nonce != null) {
|
||||
// we do not want to serve any requests (except hardcoded) that have nonces from cache
|
||||
return Mono.empty()
|
||||
}
|
||||
val common = commonRequests(key)
|
||||
if (common != null) {
|
||||
return common.map { JsonRpcResponse(it, null) }
|
||||
|
||||
@@ -283,7 +283,7 @@ class WsConnection(
|
||||
fun onRpc(msg: ResponseWSParser.WsResponse): Mono<Void> {
|
||||
return if (msg.id.isNumber()) {
|
||||
val resp = JsonRpcResponse(
|
||||
msg.value, msg.error, msg.id
|
||||
msg.value, msg.error, msg.id, null
|
||||
)
|
||||
Mono.fromCallable {
|
||||
val status = rpcReceive.tryEmitNext(resp)
|
||||
@@ -377,7 +377,7 @@ class WsConnection(
|
||||
RpcResponseError.CODE_INTERNAL_ERROR,
|
||||
"Response not received from WebSocket"
|
||||
),
|
||||
JsonRpcResponse.Id.from(originalId)
|
||||
JsonRpcResponse.Id.from(originalId), null
|
||||
)
|
||||
|
||||
return Flux.from(rpcReceive.asFlux())
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.context.Lifecycle
|
||||
import reactor.core.publisher.Mono
|
||||
import java.math.BigInteger
|
||||
import java.time.Instant
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.function.Function
|
||||
|
||||
@@ -50,7 +51,7 @@ class BitcoinGrpcUpstream(
|
||||
val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
private val client: JsonRpcGrpcClient
|
||||
) : BitcoinUpstream(
|
||||
"$parentId/${chain.chainCode}",
|
||||
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
|
||||
chain,
|
||||
UpstreamsConfig.Options.getDefaults(),
|
||||
role
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.springframework.context.Lifecycle
|
||||
import reactor.core.publisher.Mono
|
||||
import java.math.BigInteger
|
||||
import java.time.Instant
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.function.Function
|
||||
|
||||
@@ -53,7 +54,7 @@ open class EthereumGrpcUpstream(
|
||||
private val remote: ReactorBlockchainGrpc.ReactorBlockchainStub,
|
||||
private val client: JsonRpcGrpcClient
|
||||
) : EthereumUpstream(
|
||||
"$parentId/${chain.chainCode}",
|
||||
"${parentId}_${chain.chainCode.lowercase(Locale.getDefault())}",
|
||||
UpstreamsConfig.Options.getDefaults(),
|
||||
role,
|
||||
null, null
|
||||
|
||||
@@ -17,10 +17,12 @@ package io.emeraldpay.dshackle.upstream.rpcclient
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallReplySignature
|
||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
import io.emeraldpay.grpc.Chain
|
||||
@@ -60,13 +62,14 @@ class JsonRpcGrpcClient(
|
||||
}
|
||||
}
|
||||
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
val reqItem = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setId(1)
|
||||
.setMethod(key.method)
|
||||
.setPayload(ByteString.copyFrom(Global.objectMapper.writeValueAsBytes(key.params)))
|
||||
.build().let {
|
||||
req.addItems(it)
|
||||
}
|
||||
if (key.nonce != null) {
|
||||
reqItem.nonce = key.nonce
|
||||
}
|
||||
req.addItems(reqItem.build())
|
||||
|
||||
return Mono.just(key)
|
||||
.doOnNext {
|
||||
@@ -77,7 +80,12 @@ class JsonRpcGrpcClient(
|
||||
.flatMap { resp ->
|
||||
if (resp.succeed) {
|
||||
val bytes = resp.payload.toByteArray()
|
||||
Mono.just(JsonRpcResponse(bytes, null))
|
||||
val signature = if (resp.hasSignature()) {
|
||||
extractSignature(resp.signature)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
Mono.just(JsonRpcResponse(bytes, null, JsonRpcResponse.NumberId(0), signature))
|
||||
} else {
|
||||
metrics.fails.increment()
|
||||
Mono.error(
|
||||
@@ -96,5 +104,16 @@ class JsonRpcGrpcClient(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun extractSignature(resp: NativeCallReplySignature?): ResponseSigner.Signature? {
|
||||
if (resp == null || resp.signature == null || resp.signature.isEmpty || resp.upstreamId == null || resp.upstreamId.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return ResponseSigner.Signature(
|
||||
resp.signature.toByteArray(),
|
||||
resp.upstreamId,
|
||||
resp.keyId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,11 @@ import io.emeraldpay.dshackle.Global
|
||||
data class JsonRpcRequest(
|
||||
val method: String,
|
||||
val params: List<Any?>,
|
||||
val id: Int
|
||||
val id: Int,
|
||||
val nonce: Long?
|
||||
) {
|
||||
|
||||
constructor(method: String, params: List<Any?>) : this(method, params, 1)
|
||||
@JvmOverloads constructor(method: String, params: List<Any?>, nonce: Long? = null) : this(method, params, 1, nonce)
|
||||
|
||||
fun toJson(): ByteArray {
|
||||
val json = mapOf(
|
||||
@@ -62,7 +63,7 @@ data class JsonRpcRequest(
|
||||
throw IllegalStateException("Unsupported param type: ${it.asToken()}")
|
||||
}
|
||||
}
|
||||
return JsonRpcRequest(method, params, id)
|
||||
return JsonRpcRequest(method, params, id, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,18 @@ package io.emeraldpay.dshackle.upstream.rpcclient
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
class JsonRpcResponse(
|
||||
private val result: ByteArray?,
|
||||
val error: JsonRpcError?,
|
||||
val id: Id
|
||||
val id: Id,
|
||||
|
||||
/**
|
||||
* When making a request through Dshackle protocol a remote may provide its signature with the response, which we keep here
|
||||
*/
|
||||
val providedSignature: ResponseSigner.Signature? = null
|
||||
) {
|
||||
|
||||
constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, NumberId(0))
|
||||
@@ -107,7 +113,11 @@ class JsonRpcResponse(
|
||||
}
|
||||
|
||||
fun copyWithId(id: Id): JsonRpcResponse {
|
||||
return JsonRpcResponse(result, error, id)
|
||||
return JsonRpcResponse(result, error, id, providedSignature)
|
||||
}
|
||||
|
||||
fun copyWithSignature(signature: ResponseSigner.Signature): JsonRpcResponse {
|
||||
return JsonRpcResponse(result, error, id, signature)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
||||
@@ -25,11 +25,11 @@ open class ResponseRpcParser : ResponseParser<JsonRpcResponse>() {
|
||||
|
||||
override fun build(state: Preparsed): JsonRpcResponse {
|
||||
if (state.error != null) {
|
||||
return JsonRpcResponse(null, state.error, state.id ?: JsonRpcResponse.Id.from(-1))
|
||||
return JsonRpcResponse(null, state.error, state.id ?: JsonRpcResponse.Id.from(-1), null)
|
||||
}
|
||||
if (state.nullResult) {
|
||||
return JsonRpcResponse("null".toByteArray(), null, state.id ?: JsonRpcResponse.Id.from(-1))
|
||||
return JsonRpcResponse("null".toByteArray(), null, state.id ?: JsonRpcResponse.Id.from(-1), null)
|
||||
}
|
||||
return JsonRpcResponse(state.result, null, state.id ?: JsonRpcResponse.Id.from(-1))
|
||||
return JsonRpcResponse(state.result, null, state.id ?: JsonRpcResponse.Id.from(-1), null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.emeraldpay.dshackle.upstream.signature
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
|
||||
class NoSigner : ResponseSigner {
|
||||
override fun sign(nonce: Long, message: ByteArray, source: Upstream): ResponseSigner.Signature? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.emeraldpay.dshackle.upstream.signature
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
|
||||
interface ResponseSigner {
|
||||
|
||||
fun sign(nonce: Long, message: ByteArray, source: Upstream): Signature?
|
||||
|
||||
data class Signature(
|
||||
val value: ByteArray,
|
||||
val upstreamId: String,
|
||||
val keyId: Long,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Signature) return false
|
||||
|
||||
if (!value.contentEquals(other.value)) return false
|
||||
if (upstreamId != other.upstreamId) return false
|
||||
if (keyId != other.keyId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value.contentHashCode()
|
||||
result = 31 * result + upstreamId.hashCode()
|
||||
result = 31 * result + keyId.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package io.emeraldpay.dshackle.upstream.signature
|
||||
|
||||
import io.emeraldpay.dshackle.config.SignatureConfig
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.spec.ECPublicKeySpec
|
||||
import org.bouncycastle.math.ec.ECPoint
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import org.bouncycastle.util.io.pem.PemReader
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.FactoryBean
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.PublicKey
|
||||
import java.security.interfaces.ECPrivateKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
|
||||
@Repository
|
||||
open class ResponseSignerFactory(
|
||||
@Autowired private val config: SignatureConfig
|
||||
) : FactoryBean<ResponseSigner> {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(ResponseSignerFactory::class.java)
|
||||
}
|
||||
|
||||
fun readKey(algorithm: SignatureConfig.Algorithm, keyPath: String): Pair<ECPrivateKey, Long> {
|
||||
val reader = PemReader(Files.newBufferedReader(Path.of(keyPath)))
|
||||
return readKey(algorithm, reader.readPemObject())
|
||||
}
|
||||
|
||||
private fun readKey(algorithm: SignatureConfig.Algorithm, pem: PemObject): Pair<ECPrivateKey, Long> {
|
||||
val keyFactory = KeyFactory.getInstance("EC")
|
||||
val key = when (algorithm) {
|
||||
SignatureConfig.Algorithm.SECP256K1 -> {
|
||||
val keySpec = PKCS8EncodedKeySpec(pem.content)
|
||||
keyFactory.generatePrivate(keySpec)
|
||||
}
|
||||
}
|
||||
|
||||
if (key !is ECPrivateKey) {
|
||||
throw IllegalStateException("Only ECDSA SECP256K1 keys are allowed")
|
||||
}
|
||||
|
||||
if (key.params.toString() != "secp256k1 (1.3.132.0.10)") {
|
||||
throw IllegalStateException("Only SECP256K1 are allowed for signing a response")
|
||||
}
|
||||
|
||||
val publicKey = extractPublicKey(keyFactory, key)
|
||||
val id = getPublicKeyId(publicKey)
|
||||
|
||||
return Pair(key, id)
|
||||
}
|
||||
|
||||
fun extractPublicKey(keyFactory: KeyFactory, privateKey: ECPrivateKey): PublicKey {
|
||||
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
val q: ECPoint = ecSpec.g.multiply(privateKey.s)
|
||||
return keyFactory.generatePublic(ECPublicKeySpec(q, ecSpec))
|
||||
}
|
||||
|
||||
private fun getPublicKeyId(publicKey: PublicKey): Long {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val fullId = digest.digest(publicKey.encoded)
|
||||
log.info("Using key to sign responses: ${Hex.encodeHexString(fullId).substring(0..15)}")
|
||||
return ByteBuffer.wrap(fullId).asLongBuffer().get()
|
||||
}
|
||||
|
||||
override fun getObject(): ResponseSigner {
|
||||
if (!config.enabled) {
|
||||
return NoSigner()
|
||||
}
|
||||
if (config.privateKey == null) {
|
||||
log.warn("Private Key for response signature is not set")
|
||||
return NoSigner()
|
||||
}
|
||||
val key = readKey(config.algorithm, config.privateKey!!)
|
||||
return Secp256KSigner(key.first, key.second)
|
||||
}
|
||||
|
||||
override fun getObjectType(): Class<*>? {
|
||||
return ResponseSigner::class.java
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.emeraldpay.dshackle.upstream.signature
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.interfaces.ECPrivateKey
|
||||
|
||||
class Secp256KSigner(
|
||||
private val privateKey: ECPrivateKey,
|
||||
val keyId: Long,
|
||||
) : ResponseSigner {
|
||||
|
||||
companion object {
|
||||
const val SIGN_SCHEME = "SHA256withECDSA"
|
||||
const val MSG_PREFIX = "DSHACKLESIG"
|
||||
const val MSG_SEPARATOR = '/'
|
||||
}
|
||||
|
||||
override fun sign(nonce: Long, message: ByteArray, source: Upstream): ResponseSigner.Signature {
|
||||
val sig = Signature.getInstance(SIGN_SCHEME)
|
||||
sig.initSign(privateKey)
|
||||
val wrapped = wrapMessage(nonce, message, source)
|
||||
sig.update(wrapped.toByteArray())
|
||||
val value = sig.sign()
|
||||
return ResponseSigner.Signature(
|
||||
value, source.getId(), keyId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* To avoid various attacks, such as various kinds of padding and message alternating attacks,
|
||||
* we (1) tag the original message to specify the source, (2) ensure no parts of the message can affect each other
|
||||
* and (3) ensure the message cannot break the wrapping.
|
||||
*
|
||||
* We're doing that by converting the message as `"DSHACKLESIG/" || str(nonce) || "/" || hex(sha256(msg))`
|
||||
*
|
||||
* I.e.:
|
||||
* - three elements in the wrapped message
|
||||
* - separated by "/" which is not a part of any element
|
||||
* - first element is DSHACKLESIG tag
|
||||
* - second is the nonce value encode as decimal string
|
||||
* - third is SHA256 hash of the original message encoded as hex string
|
||||
*/
|
||||
fun wrapMessage(nonce: Long, message: ByteArray, source: Upstream): String {
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
// we create it with max capacity that we expect for the result, which is total lengths of its parts
|
||||
val formatterMsg = StringBuilder(11 + 1 + 18 + 1 + 64 + 1 + 64)
|
||||
formatterMsg.append(MSG_PREFIX)
|
||||
.append(MSG_SEPARATOR)
|
||||
.append(nonce.toString())
|
||||
.append(MSG_SEPARATOR)
|
||||
// We expect that the id is short enough (less than 64 symbols) and also it doesn't contain the `/` symbol
|
||||
// which is verified in UpstreamConfigReader and DefaultUpstream constructor
|
||||
.append(source.getId())
|
||||
.append(MSG_SEPARATOR)
|
||||
.append(Hex.encodeHexString(sha256.digest(message)))
|
||||
return formatterMsg.toString()
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,19 @@ import io.netty.handler.ssl.ClientAuth
|
||||
import io.netty.handler.ssl.OpenSsl
|
||||
import io.netty.handler.ssl.OpenSslServerContext
|
||||
import io.netty.handler.ssl.SslContext
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import spock.lang.Specification
|
||||
import sun.security.x509.X509CertImpl
|
||||
|
||||
import java.security.Security
|
||||
|
||||
class TlsSetupSpec extends Specification {
|
||||
|
||||
TlsSetup tlsSetup = new TlsSetup(new FileResolver(new File("src/test/resources/tls-local")))
|
||||
|
||||
def setup() {
|
||||
def setupSpec() {
|
||||
Security.addProvider(new BouncyCastleProvider())
|
||||
|
||||
// !!!!!!!!!!!!
|
||||
// run test on OS with OpenSSL installed
|
||||
// !!!!!!!!!!!!
|
||||
@@ -148,12 +153,13 @@ class TlsSetupSpec extends Specification {
|
||||
def config = new AuthConfig.ServerTlsAuth(
|
||||
enabled: true,
|
||||
certificate: "127.0.0.1.crt",
|
||||
key: "127.0.0.1.key",
|
||||
// note that JDK Security doesn't accept non-P8 keys, but with Bouncy Castle we should test with a really invalid key
|
||||
key: "127.0.0.1.invalid.key",
|
||||
)
|
||||
when:
|
||||
tlsSetup.setupServer("test", config, false)
|
||||
then:
|
||||
def t = thrown(IllegalArgumentException)
|
||||
thrown(Exception)
|
||||
}
|
||||
|
||||
def "Fail if client certificate not set but required"() {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import org.bouncycastle.util.io.pem.PemWriter
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
|
||||
class SignatureConfigReaderSpec extends Specification {
|
||||
|
||||
def "Parse enabled"() {
|
||||
setup:
|
||||
def config = "signed-response:\n" +
|
||||
" enabled: true\n" +
|
||||
" algorithm: SECP256K1\n" +
|
||||
" private-key: /root/key.pem\n"
|
||||
|
||||
when:
|
||||
def reader = new SignatureConfigReader(TestingCommons.fileResolver())
|
||||
def act = reader.read(new ByteArrayInputStream(config.bytes))
|
||||
|
||||
then:
|
||||
act.enabled
|
||||
act.privateKey == "/root/key.pem"
|
||||
act.algorithm == SignatureConfig.Algorithm.SECP256K1
|
||||
}
|
||||
|
||||
def "No path when disabled"() {
|
||||
setup:
|
||||
def config = "signed-response:\n" +
|
||||
" enabled: false\n" +
|
||||
" private-key: /root/key.pem\n"
|
||||
|
||||
when:
|
||||
def reader = new SignatureConfigReader(TestingCommons.fileResolver())
|
||||
def act = reader.read(new ByteArrayInputStream(config.bytes))
|
||||
|
||||
then:
|
||||
!act.enabled
|
||||
act.privateKey == null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class BaseHandlerSpec extends Specification {
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.items.add(request)
|
||||
call.ids[0] = 5
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
|
||||
.collectList()
|
||||
@@ -85,7 +85,7 @@ class BaseHandlerSpec extends Specification {
|
||||
def call = new ProxyCall(ProxyCall.RpcType.BATCH)
|
||||
call.items.add(request)
|
||||
call.ids[0] = 5
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, false))
|
||||
.collectList()
|
||||
@@ -116,8 +116,8 @@ class BaseHandlerSpec extends Specification {
|
||||
call.items.add(request2)
|
||||
call.ids[1] = 6
|
||||
def response = [
|
||||
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
|
||||
new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
|
||||
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||
]
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
||||
@@ -149,8 +149,8 @@ class BaseHandlerSpec extends Specification {
|
||||
call.items.add(request2)
|
||||
call.ids[1] = 6
|
||||
def response = [
|
||||
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
|
||||
new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
|
||||
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||
]
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
||||
@@ -189,8 +189,8 @@ class BaseHandlerSpec extends Specification {
|
||||
|
||||
// note there is only 2 responses
|
||||
def response = [
|
||||
new NativeCall.CallResult(1, '{"foo": 2}'.bytes, null),
|
||||
new NativeCall.CallResult(2, '{"foo": 3}'.bytes, null)
|
||||
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null),
|
||||
new NativeCall.CallResult(2, null, '{"foo": 3}'.bytes, null, null)
|
||||
]
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler, true))
|
||||
|
||||
@@ -43,7 +43,7 @@ class HttpHandlerSpec extends Specification {
|
||||
.setMethod("test_test")
|
||||
.setPayload(ByteString.copyFromUtf8("[]"))
|
||||
.build()
|
||||
def respItem = new NativeCall.CallResult(1, "100".bytes, null)
|
||||
def respItem = new NativeCall.CallResult(1, null, "100".bytes, null, null)
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||
.addItems(reqItem)
|
||||
@@ -129,7 +129,7 @@ class HttpHandlerSpec extends Specification {
|
||||
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler(), false)
|
||||
|
||||
then:
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, null, "".bytes, null, null))
|
||||
StepVerifier.create(act)
|
||||
.expectNext("hello")
|
||||
.expectComplete()
|
||||
|
||||
@@ -84,7 +84,7 @@ class WebsocketHandlerSpec extends Specification {
|
||||
|
||||
def "Respond to a single call"() {
|
||||
setup:
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null)
|
||||
|
||||
def nativeCall = Mock(NativeCall) {
|
||||
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
|
||||
@@ -85,7 +85,7 @@ class WriteRpcJsonSpec extends Specification {
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 105
|
||||
def data = [
|
||||
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
|
||||
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null)
|
||||
]
|
||||
when:
|
||||
def act = writer.toJson(call, data[0])
|
||||
@@ -98,7 +98,7 @@ class WriteRpcJsonSpec extends Specification {
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 1
|
||||
def data = [
|
||||
new NativeCall.CallResult(1, null, new NativeCall.CallError(1, "Internal Error", null))
|
||||
new NativeCall.CallResult(1, null, null, new NativeCall.CallError(1, "Internal Error", null), null)
|
||||
]
|
||||
when:
|
||||
def act = writer.toJson(call, data[0])
|
||||
@@ -111,7 +111,7 @@ class WriteRpcJsonSpec extends Specification {
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = "aaa"
|
||||
def data = [
|
||||
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
|
||||
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null)
|
||||
]
|
||||
when:
|
||||
def act = writer.toJson(call, data[0])
|
||||
@@ -126,9 +126,9 @@ class WriteRpcJsonSpec extends Specification {
|
||||
call.ids[2] = 11
|
||||
call.ids[3] = 15
|
||||
def data = [
|
||||
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null),
|
||||
new NativeCall.CallResult(2, null, new NativeCall.CallError(2, "oops", null)),
|
||||
new NativeCall.CallResult(3, '{"hash": "0x2484f459dc"}'.bytes, null),
|
||||
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null),
|
||||
new NativeCall.CallResult(2, null, null, new NativeCall.CallError(2, "oops", null), null),
|
||||
new NativeCall.CallResult(3, null, '{"hash": "0x2484f459dc"}'.bytes, null, null),
|
||||
]
|
||||
when:
|
||||
def act = Flux.fromIterable(data)
|
||||
@@ -154,7 +154,7 @@ class WriteRpcJsonSpec extends Specification {
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 10
|
||||
def data = [
|
||||
new NativeCall.CallResult(1, '"0x1"'.bytes, null),
|
||||
new NativeCall.CallResult(1, null, '"0x1"'.bytes, null, null),
|
||||
]
|
||||
when:
|
||||
def act = Flux.fromIterable(data)
|
||||
|
||||
@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import spock.lang.Specification
|
||||
|
||||
class AlwaysQuorumSpec extends Specification {
|
||||
@@ -26,7 +27,7 @@ class AlwaysQuorumSpec extends Specification {
|
||||
def quorum = new AlwaysQuorum()
|
||||
def up = Stub(Upstream)
|
||||
when:
|
||||
quorum.record(new JsonRpcException(1, "test"), up)
|
||||
quorum.record(new JsonRpcException(1, "test"), null, up)
|
||||
then:
|
||||
quorum.isFailed()
|
||||
!quorum.isResolved()
|
||||
@@ -41,10 +42,11 @@ class AlwaysQuorumSpec extends Specification {
|
||||
def quorum = new AlwaysQuorum()
|
||||
def up = Stub(Upstream)
|
||||
when:
|
||||
quorum.record("123".bytes, up)
|
||||
quorum.record("123".bytes, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
|
||||
then:
|
||||
quorum.isResolved()
|
||||
quorum.getResult() == "123".bytes
|
||||
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
|
||||
!quorum.isFailed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,21 +43,21 @@ class BroadcastQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream1)
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
|
||||
|
||||
when:
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
|
||||
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
|
||||
then:
|
||||
1 * q.recordError(_, _, _)
|
||||
1 * q.recordError(_, _, _, _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||
}
|
||||
@@ -75,21 +75,21 @@ class BroadcastQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal error"), upstream1)
|
||||
q.record(new JsonRpcException(1, "Internal error"), null, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordError(_, _, _)
|
||||
1 * q.recordError(_, _, _, _)
|
||||
|
||||
when:
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, upstream2)
|
||||
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
|
||||
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
|
||||
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
|
||||
then:
|
||||
1 * q.recordError(_, _, _)
|
||||
1 * q.recordError(_, _, _, _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
|
||||
}
|
||||
@@ -99,19 +99,19 @@ class BroadcastQuorumSpec extends Specification {
|
||||
def quorum = new BroadcastQuorum(3)
|
||||
def up = Stub(Upstream)
|
||||
when:
|
||||
quorum.record(new JsonRpcException(1, "test 1"), up)
|
||||
quorum.record(new JsonRpcException(1, "test 1"), null, up)
|
||||
then:
|
||||
!quorum.isFailed()
|
||||
!quorum.isResolved()
|
||||
|
||||
when:
|
||||
quorum.record(new JsonRpcException(1, "test 2"), up)
|
||||
quorum.record(new JsonRpcException(1, "test 2"), null, up)
|
||||
then:
|
||||
!quorum.isFailed()
|
||||
!quorum.isResolved()
|
||||
|
||||
when:
|
||||
quorum.record(new JsonRpcException(1, "test 3"), up)
|
||||
quorum.record(new JsonRpcException(1, "test 3"), null, up)
|
||||
then:
|
||||
quorum.isFailed()
|
||||
!quorum.isResolved()
|
||||
|
||||
@@ -38,22 +38,23 @@ class NonEmptyQuorumSpec extends Specification {
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream2)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream3)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
|
||||
then:
|
||||
q.isFailed()
|
||||
!q.isResolved()
|
||||
q.signature == null
|
||||
}
|
||||
|
||||
def "Fail first if not error"() {
|
||||
@@ -70,7 +71,7 @@ class NonEmptyQuorumSpec extends Specification {
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record('"0x11"'.bytes, upstream1)
|
||||
q.record('"0x11"'.bytes, null, upstream1)
|
||||
then:
|
||||
q.isResolved()
|
||||
!q.isFailed()
|
||||
@@ -90,14 +91,14 @@ class NonEmptyQuorumSpec extends Specification {
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||
then:
|
||||
!q.isFailed()
|
||||
!q.isResolved()
|
||||
|
||||
q.signature == null
|
||||
|
||||
when:
|
||||
q.record('"0x11"'.bytes, upstream2)
|
||||
q.record('"0x11"'.bytes, null, upstream2)
|
||||
then:
|
||||
q.isResolved()
|
||||
!q.isFailed()
|
||||
@@ -117,14 +118,14 @@ class NonEmptyQuorumSpec extends Specification {
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record('null'.bytes, upstream2)
|
||||
q.record('null'.bytes, null, upstream2)
|
||||
then:
|
||||
!q.isFailed()
|
||||
!q.isResolved()
|
||||
|
||||
|
||||
when:
|
||||
q.record('"0x11"'.bytes, upstream2)
|
||||
q.record('"0x11"'.bytes, null, upstream2)
|
||||
then:
|
||||
q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
@@ -43,21 +43,21 @@ class NonceQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record('"0x10"'.bytes, upstream1)
|
||||
q.record('"0x10"'.bytes, null, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0x10", _)
|
||||
1 * q.recordValue(_, "0x10", _, _)
|
||||
|
||||
when:
|
||||
q.record('"0x11"'.bytes, upstream2)
|
||||
q.record('"0x11"'.bytes, null, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0x11", _)
|
||||
1 * q.recordValue(_, "0x11", _, _)
|
||||
|
||||
when:
|
||||
q.record('"0x10"'.bytes, upstream3)
|
||||
q.record('"0x10"'.bytes, null, upstream3)
|
||||
then:
|
||||
1 * q.recordValue(_, "0x10", _)
|
||||
1 * q.recordValue(_, "0x10", _, _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Object) == "0x11"
|
||||
}
|
||||
@@ -75,27 +75,27 @@ class NonceQuorumSpec extends Specification {
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordError(_, _, _)
|
||||
1 * q.recordError(_, _, _, _)
|
||||
|
||||
when:
|
||||
q.record('"0x11"'.bytes, upstream2)
|
||||
q.record('"0x11"'.bytes, null, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
1 * q.recordValue(_, "0x11", _)
|
||||
1 * q.recordValue(_, "0x11", _, _)
|
||||
|
||||
when:
|
||||
q.record('"0x10"'.bytes, upstream3)
|
||||
q.record('"0x10"'.bytes, null, upstream3)
|
||||
then:
|
||||
1 * q.recordValue(_, "0x10", _)
|
||||
1 * q.recordValue(_, "0x10", _, _)
|
||||
!q.isResolved()
|
||||
|
||||
when:
|
||||
q.record('"0x11"'.bytes, upstream1)
|
||||
q.record('"0x11"'.bytes, null, upstream1)
|
||||
then:
|
||||
1 * q.recordValue(_, "0x11", _)
|
||||
1 * q.recordValue(_, "0x11", _, _)
|
||||
q.isResolved()
|
||||
objectMapper.readValue(q.result, Object) == "0x11"
|
||||
}
|
||||
@@ -114,23 +114,24 @@ class NonceQuorumSpec extends Specification {
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream1)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream1)
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream2)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream2)
|
||||
then:
|
||||
!q.isResolved()
|
||||
!q.isFailed()
|
||||
|
||||
when:
|
||||
q.record(new JsonRpcException(1, "Internal"), upstream3)
|
||||
q.record(new JsonRpcException(1, "Internal"), null, upstream3)
|
||||
then:
|
||||
q.isFailed()
|
||||
!q.isResolved()
|
||||
q.getError() != null
|
||||
q.getError().message == "Internal"
|
||||
q.signature == null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import spock.lang.Specification
|
||||
|
||||
@@ -31,7 +32,7 @@ class NotLaggingQuorumSpec extends Specification {
|
||||
def quorum = new NotLaggingQuorum(1)
|
||||
|
||||
when:
|
||||
quorum.record(value, up)
|
||||
quorum.record(value, null, up)
|
||||
then:
|
||||
1 * up.getLag() >> 0
|
||||
quorum.isResolved()
|
||||
@@ -39,6 +40,22 @@ class NotLaggingQuorumSpec extends Specification {
|
||||
quorum.result == value
|
||||
}
|
||||
|
||||
def "Keeps signature"() {
|
||||
setup:
|
||||
def up = Mock(Upstream)
|
||||
def value = "foo".getBytes()
|
||||
def quorum = new NotLaggingQuorum(1)
|
||||
|
||||
when:
|
||||
quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
|
||||
then:
|
||||
1 * up.getLag() >> 0
|
||||
quorum.isResolved()
|
||||
!quorum.isFailed()
|
||||
quorum.result == value
|
||||
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
|
||||
}
|
||||
|
||||
def "Resolves if ok lag"() {
|
||||
setup:
|
||||
def up = Mock(Upstream)
|
||||
@@ -46,7 +63,7 @@ class NotLaggingQuorumSpec extends Specification {
|
||||
def quorum = new NotLaggingQuorum(1)
|
||||
|
||||
when:
|
||||
quorum.record(value, up)
|
||||
quorum.record(value, null, up)
|
||||
then:
|
||||
1 * up.getLag() >> 1
|
||||
quorum.isResolved()
|
||||
@@ -61,7 +78,7 @@ class NotLaggingQuorumSpec extends Specification {
|
||||
def quorum = new NotLaggingQuorum(1)
|
||||
|
||||
when:
|
||||
quorum.record(value, up)
|
||||
quorum.record(value, null, up)
|
||||
then:
|
||||
1 * up.getLag() >> 2
|
||||
!quorum.isResolved()
|
||||
@@ -75,7 +92,7 @@ class NotLaggingQuorumSpec extends Specification {
|
||||
def quorum = new NotLaggingQuorum(1)
|
||||
|
||||
when:
|
||||
quorum.record(new JsonRpcException(-100, "test error"), up)
|
||||
quorum.record(new JsonRpcException(-100, "test error"), null, up)
|
||||
then:
|
||||
1 * up.getLag() >> 1
|
||||
!quorum.isResolved()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.quorum
|
||||
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import org.jetbrains.annotations.NotNull
|
||||
@@ -66,12 +66,14 @@ class ValueAwareQuorumSpec extends Specification {
|
||||
}
|
||||
|
||||
@Override
|
||||
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @NotNull Upstream upstream) {
|
||||
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @NotNull Upstream upstream) {
|
||||
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) {
|
||||
|
||||
}
|
||||
|
||||
@@ -85,6 +87,11 @@ class ValueAwareQuorumSpec extends Specification {
|
||||
return false
|
||||
}
|
||||
|
||||
@Override
|
||||
ResponseSigner.Signature getSignature() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getResult() {
|
||||
return new byte[0]
|
||||
|
||||
@@ -31,11 +31,11 @@ import io.emeraldpay.dshackle.upstream.Head
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.MultistreamHolder
|
||||
import io.emeraldpay.dshackle.upstream.Upstream
|
||||
import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
|
||||
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.etherjar.rpc.RpcResponseError
|
||||
@@ -51,6 +51,16 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
ObjectMapper objectMapper = Global.objectMapper
|
||||
|
||||
def nativeCall(MultistreamHolder upstreams = null, ResponseSigner signer = null) {
|
||||
if (upstreams == null) {
|
||||
upstreams = Stub(MultistreamHolder)
|
||||
}
|
||||
if (signer == null) {
|
||||
signer = Stub(ResponseSigner)
|
||||
}
|
||||
new NativeCall(upstreams, signer)
|
||||
}
|
||||
|
||||
def "Tries router first"() {
|
||||
def routedApi = Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.just(new JsonRpcResponse("1".bytes, null))
|
||||
@@ -58,11 +68,10 @@ class NativeCallSpec extends Specification {
|
||||
def upstream = Mock(Multistream) {
|
||||
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||
}
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(
|
||||
1, upstream, Selector.empty, new AlwaysQuorum(),
|
||||
1, null, upstream, Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.ParsedCallDetails("eth_test", [])
|
||||
)
|
||||
|
||||
@@ -72,6 +81,7 @@ class NativeCallSpec extends Specification {
|
||||
act.result == "1".bytes
|
||||
}
|
||||
|
||||
|
||||
def "Return error if router denied the requests"() {
|
||||
def routedApi = Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_test", [])) >> Mono.error(new RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Test message"))
|
||||
@@ -79,11 +89,10 @@ class NativeCallSpec extends Specification {
|
||||
def upstream = Mock(Multistream) {
|
||||
1 * getRoutedApi(_) >> Mono.just(routedApi)
|
||||
}
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(
|
||||
15, upstream, Selector.empty, new AlwaysQuorum(),
|
||||
15, null, upstream, Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.ParsedCallDetails("eth_test", [])
|
||||
)
|
||||
|
||||
@@ -102,13 +111,13 @@ class NativeCallSpec extends Specification {
|
||||
setup:
|
||||
def quorum = new AlwaysQuorum()
|
||||
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
||||
def nativeCall = nativeCall()
|
||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, 1))
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, null, 1))
|
||||
}
|
||||
}
|
||||
def call = new NativeCall.ValidCallContext(1, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
|
||||
def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
when:
|
||||
@@ -116,19 +125,20 @@ class NativeCallSpec extends Specification {
|
||||
def act = objectMapper.readValue(resp.result, Object)
|
||||
then:
|
||||
act == "foo"
|
||||
resp.nonce == 10
|
||||
}
|
||||
|
||||
def "Returns error if no quorum"() {
|
||||
setup:
|
||||
def quorum = new AlwaysQuorum()
|
||||
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
||||
def nativeCall = nativeCall()
|
||||
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * read(_) >> Mono.empty()
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.empty()
|
||||
}
|
||||
}
|
||||
def call = new NativeCall.ValidCallContext(1, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
|
||||
def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
when:
|
||||
@@ -136,7 +146,7 @@ class NativeCallSpec extends Specification {
|
||||
then:
|
||||
StepVerifier.create(resp)
|
||||
.expectNextMatches { result ->
|
||||
result.isError()
|
||||
result.isError() && result.nonce == 10
|
||||
}
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
@@ -144,8 +154,7 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Packs call exception into response with id"() {
|
||||
setup:
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
when:
|
||||
def resp = nativeCall.processException(new NativeCall.CallFailure(5, new IllegalArgumentException("test test")))
|
||||
then:
|
||||
@@ -161,8 +170,7 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Packs unknown exception into response"() {
|
||||
setup:
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
when:
|
||||
def resp = nativeCall.processException(new IllegalArgumentException("test test"))
|
||||
then:
|
||||
@@ -177,13 +185,12 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Builds normal response"() {
|
||||
setup:
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
||||
|
||||
when:
|
||||
def resp = nativeCall.buildResponse(
|
||||
new NativeCall.CallResult(1561, objectMapper.writeValueAsBytes(json), null)
|
||||
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, null)
|
||||
)
|
||||
then:
|
||||
resp.id == 1561
|
||||
@@ -191,10 +198,28 @@ class NativeCallSpec extends Specification {
|
||||
objectMapper.readValue(resp.payload.toByteArray(), Map.class) == [jsonrpc:"2.0", id:1, result: "foo"]
|
||||
}
|
||||
|
||||
def "Builds response with signature"() {
|
||||
setup:
|
||||
def nativeCall = nativeCall()
|
||||
def json = [jsonrpc:"2.0", id:1, result: "foo"]
|
||||
|
||||
when:
|
||||
def resp = nativeCall.buildResponse(
|
||||
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, new ResponseSigner.Signature("sig1".bytes, "test", 100))
|
||||
)
|
||||
then:
|
||||
resp.id == 1561
|
||||
resp.succeed
|
||||
resp.signature.nonce == 10
|
||||
resp.signature.signature.toByteArray() == "sig1".bytes
|
||||
resp.signature.keyId == 100
|
||||
resp.signature.upstreamId == "test"
|
||||
objectMapper.readValue(resp.payload.toByteArray(), Map.class) == [jsonrpc:"2.0", id:1, result: "foo"]
|
||||
}
|
||||
|
||||
def "Returns error for invalid chain"() {
|
||||
setup:
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChainValue(0)
|
||||
@@ -210,7 +235,6 @@ class NativeCallSpec extends Specification {
|
||||
then:
|
||||
StepVerifier.create(resp)
|
||||
.expectErrorMatches({t -> t instanceof NativeCall.CallFailure && t.id == 0})
|
||||
// .expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
@@ -219,7 +243,7 @@ class NativeCallSpec extends Specification {
|
||||
def upstreams = Mock(MultistreamHolder) {
|
||||
_ * it.observeChains() >> Flux.empty()
|
||||
}
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall(upstreams)
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChainValue(Chain.TESTNET_MORDEN.id)
|
||||
@@ -245,13 +269,14 @@ class NativeCallSpec extends Specification {
|
||||
def upstreams = Mock(MultistreamHolder) {
|
||||
_ * it.observeChains() >> Flux.empty()
|
||||
}
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall(upstreams)
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||
.addItems(
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setId(1)
|
||||
.setNonce(10)
|
||||
.setMethod("eth_test")
|
||||
.setPayload(ByteString.copyFromUtf8("[]"))
|
||||
)
|
||||
@@ -263,6 +288,7 @@ class NativeCallSpec extends Specification {
|
||||
act.size() == 1
|
||||
with(act[0]) {
|
||||
id == 1
|
||||
nonce == 10
|
||||
payload.method == "eth_test"
|
||||
payload.params == "[]"
|
||||
}
|
||||
@@ -273,7 +299,7 @@ class NativeCallSpec extends Specification {
|
||||
def upstreams = Mock(MultistreamHolder) {
|
||||
_ * it.observeChains() >> Flux.empty()
|
||||
}
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall(upstreams)
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||
@@ -300,7 +326,7 @@ class NativeCallSpec extends Specification {
|
||||
def upstreams = Mock(MultistreamHolder) {
|
||||
_ * it.observeChains() >> Flux.empty()
|
||||
}
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall(upstreams)
|
||||
|
||||
def item = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setId(1)
|
||||
@@ -338,7 +364,7 @@ class NativeCallSpec extends Specification {
|
||||
def multistreamHolder = Mock(MultistreamHolder) {
|
||||
_ * it.observeChains() >> Flux.empty()
|
||||
}
|
||||
def nativeCall = new NativeCall(multistreamHolder)
|
||||
def nativeCall = nativeCall(multistreamHolder)
|
||||
|
||||
def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
|
||||
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
|
||||
@@ -365,8 +391,8 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Parse empty params"() {
|
||||
setup:
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
def nativeCall = nativeCall()
|
||||
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.RawCallDetails("eth_test", "[]"))
|
||||
when:
|
||||
def act = nativeCall.parseParams(ctx)
|
||||
@@ -378,8 +404,8 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Parse none params"() {
|
||||
setup:
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
def nativeCall = nativeCall()
|
||||
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.RawCallDetails("eth_test", ""))
|
||||
when:
|
||||
def act = nativeCall.parseParams(ctx)
|
||||
@@ -391,8 +417,8 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Parse single param"() {
|
||||
setup:
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
def nativeCall = nativeCall()
|
||||
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.RawCallDetails("eth_test", "[false]"))
|
||||
when:
|
||||
def act = nativeCall.parseParams(ctx)
|
||||
@@ -404,8 +430,8 @@ class NativeCallSpec extends Specification {
|
||||
|
||||
def "Parse multi param"() {
|
||||
setup:
|
||||
def nativeCall = new NativeCall(Stub(MultistreamHolder))
|
||||
def ctx = new NativeCall.ValidCallContext(1, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
def nativeCall = nativeCall()
|
||||
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.RawCallDetails("eth_test", "[false, 123]"))
|
||||
when:
|
||||
def act = nativeCall.parseParams(ctx)
|
||||
@@ -419,12 +445,11 @@ class NativeCallSpec extends Specification {
|
||||
//TODO
|
||||
def "Calls cache before remote"() {
|
||||
setup:
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
def api = TestingCommons.api()
|
||||
def upstream = TestingCommons.multistream(api)
|
||||
|
||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10,
|
||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10, null,
|
||||
upstream,
|
||||
Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
@@ -438,11 +463,10 @@ class NativeCallSpec extends Specification {
|
||||
//TODO
|
||||
def "Uses cached value"() {
|
||||
setup:
|
||||
def upstreams = Stub(MultistreamHolder)
|
||||
def nativeCall = new NativeCall(upstreams)
|
||||
def nativeCall = nativeCall()
|
||||
def upstream = TestingCommons.multistream(TestingCommons.api())
|
||||
|
||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10,
|
||||
def ctx = new NativeCall.ValidCallContext<NativeCall.ParsedCallDetails>(10, null,
|
||||
upstream,
|
||||
Selector.empty, new AlwaysQuorum(),
|
||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||
|
||||
@@ -91,7 +91,6 @@ class TrackEthereumTxSpec extends Specification {
|
||||
.setBlock(
|
||||
Common.BlockInfo.newBuilder()
|
||||
.setHeight(blockJson.number)
|
||||
.setWeight(ByteString.copyFrom(blockJson.totalDifficulty.toByteArray()))
|
||||
.setBlockId(blockJson.hash.toHex().substring(2))
|
||||
.setTimestamp(blockJson.timestamp.toEpochMilli())
|
||||
).build()
|
||||
@@ -280,7 +279,6 @@ class TrackEthereumTxSpec extends Specification {
|
||||
.setBlock(
|
||||
Common.BlockInfo.newBuilder()
|
||||
.setHeight(blocks[2].number)
|
||||
.setWeight(ByteString.copyFrom(blocks[2].totalDifficulty.toByteArray()))
|
||||
.setBlockId(blocks[2].hash.toHex().substring(2))
|
||||
.setTimestamp(blocks[2].timestamp.toEpochMilli())
|
||||
)
|
||||
|
||||
@@ -108,7 +108,7 @@ class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
}
|
||||
error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked")
|
||||
}
|
||||
return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id))
|
||||
return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id), null)
|
||||
} as Callable<JsonRpcResponse>
|
||||
return Mono.fromCallable(call)
|
||||
}
|
||||
@@ -323,7 +323,7 @@ class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
|
||||
}
|
||||
|
||||
@Override
|
||||
def <S> NettyOutbound sendUsing(Callable<? extends S> sourceInput, BiFunction<? super Connection, ? super S, ?> mappedInput, Consumer<? super S> sourceCleanup) {
|
||||
<S> NettyOutbound sendUsing(Callable<? extends S> sourceInput, BiFunction<? super Connection, ? super S, ?> mappedInput, Consumer<? super S> sourceCleanup) {
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ class FilteredApisSpec extends Specification {
|
||||
setup:
|
||||
List<Upstream> standard = (0..1).collect {
|
||||
TestingCommons.upstream(
|
||||
it.toString(),
|
||||
"test_" + it,
|
||||
new EthereumApiStub(it)
|
||||
)
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class FilteredApisSpec extends Specification {
|
||||
setup:
|
||||
List<Upstream> standard = (0..1).collect {
|
||||
TestingCommons.upstream(
|
||||
it.toString(),
|
||||
"test_" + it,
|
||||
new EthereumApiStub(it)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ class MultistreamSpec extends Specification {
|
||||
|
||||
def "Filter Best Status accepts better input"() {
|
||||
setup:
|
||||
def up1 = TestingCommons.upstream("1")
|
||||
def up2 = TestingCommons.upstream("2")
|
||||
def up1 = TestingCommons.upstream("test-1")
|
||||
def up2 = TestingCommons.upstream("test-2")
|
||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||
def filter = new Multistream.FilterBestAvailability()
|
||||
def update0 = new Multistream.UpstreamStatus(
|
||||
@@ -87,8 +87,8 @@ class MultistreamSpec extends Specification {
|
||||
|
||||
def "Filter Best Status declines worse input"() {
|
||||
setup:
|
||||
def up1 = TestingCommons.upstream("1")
|
||||
def up2 = TestingCommons.upstream("2")
|
||||
def up1 = TestingCommons.upstream("test-1")
|
||||
def up2 = TestingCommons.upstream("test-2")
|
||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||
def filter = new Multistream.FilterBestAvailability()
|
||||
def update0 = new Multistream.UpstreamStatus(
|
||||
@@ -106,7 +106,7 @@ class MultistreamSpec extends Specification {
|
||||
|
||||
def "Filter Best Status accepts worse input from same upstream"() {
|
||||
setup:
|
||||
def up = TestingCommons.upstream("1")
|
||||
def up = TestingCommons.upstream("test-1")
|
||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||
def filter = new Multistream.FilterBestAvailability()
|
||||
def update0 = new Multistream.UpstreamStatus(
|
||||
@@ -124,8 +124,8 @@ class MultistreamSpec extends Specification {
|
||||
|
||||
def "Filter Best Status accepts any input if existing is outdated"() {
|
||||
setup:
|
||||
def up1 = TestingCommons.upstream("1")
|
||||
def up2 = TestingCommons.upstream("2")
|
||||
def up1 = TestingCommons.upstream("test-1")
|
||||
def up2 = TestingCommons.upstream("test-2")
|
||||
def time0 = Instant.now() - Duration.ofSeconds(90)
|
||||
def filter = new Multistream.FilterBestAvailability()
|
||||
def update0 = new Multistream.UpstreamStatus(
|
||||
@@ -143,9 +143,9 @@ class MultistreamSpec extends Specification {
|
||||
|
||||
def "Filter Best Status declines same status"() {
|
||||
setup:
|
||||
def up1 = TestingCommons.upstream("1")
|
||||
def up2 = TestingCommons.upstream("2")
|
||||
def up3 = TestingCommons.upstream("3")
|
||||
def up1 = TestingCommons.upstream("test-1")
|
||||
def up2 = TestingCommons.upstream("test-2")
|
||||
def up3 = TestingCommons.upstream("test-3")
|
||||
def time0 = Instant.now() - Duration.ofSeconds(60)
|
||||
def filter = new Multistream.FilterBestAvailability()
|
||||
def update0 = new Multistream.UpstreamStatus(
|
||||
@@ -172,7 +172,7 @@ class MultistreamSpec extends Specification {
|
||||
|
||||
def "Call postprocess after api use"() {
|
||||
setup:
|
||||
def request = new JsonRpcRequest("test_foo", [1], 1)
|
||||
def request = new JsonRpcRequest("test_foo", [1], 1, null)
|
||||
|
||||
def api = TestingCommons.api()
|
||||
api.answer("test_foo", [1], "test")
|
||||
|
||||
@@ -13,7 +13,7 @@ class RequestPostprocessorSpec extends Specification {
|
||||
|
||||
def "Wrappers calls onReceive for a value"() {
|
||||
setup:
|
||||
def request = new JsonRpcRequest("test_foo", [1], 1)
|
||||
def request = new JsonRpcRequest("test_foo", [1], 1, null)
|
||||
def processor = Mock(RequestPostprocessor)
|
||||
def api = TestingCommons.api()
|
||||
api.answer("test_foo", [1], "test")
|
||||
@@ -30,7 +30,7 @@ class RequestPostprocessorSpec extends Specification {
|
||||
|
||||
def "Wrappers doesn't call onReceive for no value"() {
|
||||
setup:
|
||||
def request = new JsonRpcRequest("test_foo", [1], 1)
|
||||
def request = new JsonRpcRequest("test_foo", [1], 1, null)
|
||||
def processor = Mock(RequestPostprocessor)
|
||||
Reader<JsonRpcRequest, JsonRpcResponse> reader = Mock(Reader) {
|
||||
1 * it.read(request) >> Mono.empty()
|
||||
|
||||
+14
-14
@@ -50,10 +50,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes(json), 1
|
||||
Global.objectMapper.writeValueAsBytes(json), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -81,10 +81,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes(null), 1
|
||||
Global.objectMapper.writeValueAsBytes(null), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -116,10 +116,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes(json), 1
|
||||
Global.objectMapper.writeValueAsBytes(json), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -152,10 +152,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes(json), 1
|
||||
Global.objectMapper.writeValueAsBytes(json), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -183,10 +183,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes(null), 1
|
||||
Global.objectMapper.writeValueAsBytes(null), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -214,10 +214,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes("0x100"), 1
|
||||
Global.objectMapper.writeValueAsBytes("0x100"), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -246,10 +246,10 @@ class EthereumDirectReaderSpec extends Specification {
|
||||
up, Caches.default(), new CurrentBlockCache(), calls
|
||||
)
|
||||
reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
|
||||
1 * create(_, _) >> Mock(Reader) {
|
||||
1 * create(_, _, _) >> Mock(Reader) {
|
||||
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just(
|
||||
new QuorumRpcReader.Result(
|
||||
Global.objectMapper.writeValueAsBytes("0x100"), 1
|
||||
Global.objectMapper.writeValueAsBytes("0x100"), null, 1
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,25 @@ class LocalCallRouterSpec extends Specification {
|
||||
act.resultAsProcessedString == "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
def "Returns empty if nonce set"() {
|
||||
setup:
|
||||
def methods = new DefaultEthereumMethods(Chain.ETHEREUM)
|
||||
def router = new LocalCallRouter(
|
||||
new EthereumReader(
|
||||
TestingCommons.multistream(TestingCommons.api()),
|
||||
Caches.default(),
|
||||
ConstantFactory.constantFactory(new DefaultEthereumMethods(Chain.ETHEREUM))
|
||||
),
|
||||
methods,
|
||||
new EmptyHead()
|
||||
)
|
||||
when:
|
||||
def act = router.read(new JsonRpcRequest("eth_getTransactionByHash", ["test"], 10))
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "getBlockByNumber with latest uses latest id"() {
|
||||
setup:
|
||||
def head = Mock(Head) {
|
||||
|
||||
@@ -83,7 +83,7 @@ class WsConnectionSpec extends Specification {
|
||||
|
||||
when:
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
@@ -105,7 +105,7 @@ class WsConnectionSpec extends Specification {
|
||||
|
||||
when:
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
@@ -129,7 +129,7 @@ class WsConnectionSpec extends Specification {
|
||||
|
||||
when:
|
||||
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15))
|
||||
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null))
|
||||
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
|
||||
+10
-10
@@ -30,7 +30,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
when:
|
||||
def act = resp1.equals(resp2)
|
||||
then:
|
||||
act == true
|
||||
act
|
||||
}
|
||||
|
||||
def "Extract processed string without quoted"() {
|
||||
@@ -49,7 +49,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Fails to extract processed string if not quoted"() {
|
||||
when:
|
||||
def act = new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString
|
||||
new JsonRpcResponse("{\"hello\": 1}".bytes, null).resultAsProcessedString
|
||||
then:
|
||||
thrown(IllegalStateException)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize int id and null result"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.NumberId(1))
|
||||
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.NumberId(1), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -72,7 +72,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize int id and string result"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.NumberId(10))
|
||||
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.NumberId(10), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -81,7 +81,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize int id and object result"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.NumberId(101))
|
||||
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.NumberId(101), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize int id and error"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.NumberId(101))
|
||||
def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.NumberId(101), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -99,7 +99,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize string id and null result"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"))
|
||||
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -108,7 +108,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize string id and string result"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"))
|
||||
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -117,7 +117,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
|
||||
def "Serialize string id and object result"() {
|
||||
setup:
|
||||
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"))
|
||||
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
@@ -128,7 +128,7 @@ class JsonRpcResponseSpec extends Specification {
|
||||
setup:
|
||||
def json = new JsonRpcResponse(null,
|
||||
new JsonRpcError(-32041, "Oooops"),
|
||||
new JsonRpcResponse.StringId("9kbo29gkaasf"))
|
||||
new JsonRpcResponse.StringId("9kbo29gkaasf"), null)
|
||||
when:
|
||||
def act = objectMapper.writeValueAsString(json)
|
||||
then:
|
||||
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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-----
|
||||
Reference in New Issue
Block a user