solution: connects to remote dshackle

This commit is contained in:
Igor Artamonov
2019-07-25 22:53:02 -04:00
parent 499a92e4d6
commit fd8c35bec2
31 changed files with 1320 additions and 312 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ build/
out/ out/
*.iml *.iml
*.yaml *.yaml
testsetup/

View File

@@ -44,7 +44,7 @@ configurations {
} }
dependencies { dependencies {
compile "io.emeraldpay:emerald-grpc:0.4" compile "io.emeraldpay:emerald-grpc:0.6-SNAPSHOT"
compile "io.grpc:grpc-protobuf:${grpcVersion}" compile "io.grpc:grpc-protobuf:${grpcVersion}"
compile "io.grpc:grpc-stub:${grpcVersion}" compile "io.grpc:grpc-stub:${grpcVersion}"
@@ -63,6 +63,7 @@ dependencies {
compile 'io.projectreactor:reactor-core:3.2.9.RELEASE' compile 'io.projectreactor:reactor-core:3.2.9.RELEASE'
compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE' compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE'
compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1' compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1'
compile 'com.salesforce.servicelibs:reactor-grpc:0.10.0'
compile 'org.yaml:snakeyaml:1.24' compile 'org.yaml:snakeyaml:1.24'
@@ -93,7 +94,9 @@ dependencies {
testCompile "org.codehaus.groovy:groovy:$groovyVersion" testCompile "org.codehaus.groovy:groovy:$groovyVersion"
testCompile 'cglib:cglib-nodep:3.2.12'
testCompile "org.spockframework:spock-core:$spockVersion" testCompile "org.spockframework:spock-core:$spockVersion"
testCompile "io.grpc:grpc-testing:${grpcVersion}"
} }
compileKotlin { compileKotlin {

View File

@@ -1,7 +1,7 @@
package io.emeraldpay.dshackle.config; package io.emeraldpay.dshackle.config;
import org.yaml.snakeyaml.TypeDescription; import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.introspector.GenericProperty;
import org.yaml.snakeyaml.introspector.MethodProperty; import org.yaml.snakeyaml.introspector.MethodProperty;
import org.yaml.snakeyaml.introspector.Property; import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.introspector.PropertySubstitute; import org.yaml.snakeyaml.introspector.PropertySubstitute;
@@ -12,6 +12,10 @@ import org.yaml.snakeyaml.nodes.ScalarNode;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.beans.IntrospectionException; import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor; import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -201,6 +205,8 @@ public class UpstreamsConfig {
public static class Endpoint { public static class Endpoint {
private EndpointType type; private EndpointType type;
private URI url; private URI url;
private String host;
private int port;
@Nullable @Nullable
private Auth auth; private Auth auth;
private Boolean enabled = true; private Boolean enabled = true;
@@ -248,6 +254,22 @@ public class UpstreamsConfig {
public void setOrigin(@Nullable URI origin) { public void setOrigin(@Nullable URI origin) {
this.origin = origin; this.origin = origin;
} }
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
} }
public static class Auth { public static class Auth {
@@ -262,7 +284,12 @@ public class UpstreamsConfig {
} }
} }
public static class BasicAuth extends Auth { public static interface WithKey {
public String getKey();
public void setKey(String key);
}
public static class BasicAuth extends Auth implements WithKey {
private String key; private String key;
public String getKey() { public String getKey() {
@@ -274,18 +301,72 @@ public class UpstreamsConfig {
} }
} }
public static class TlsAuth extends Auth implements WithKey {
private String ca;
private String certificate;
private String key;
public String getCa() {
return ca;
}
public void setCa(String ca) {
this.ca = ca;
}
public String getCertificate() {
return certificate;
}
public void setCertificate(String certificate) {
this.certificate = certificate;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static class AuthYaml extends TypeDescription { public static class AuthYaml extends TypeDescription {
public AuthYaml() { public AuthYaml() {
super(Auth.class); super(Auth.class);
}
public Class getImpl() {
try {
Field f = TypeDescription.class.getDeclaredField("impl");
f.setAccessible(true);
return (Class) f.get(this);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
} }
@Override @Override
public Property getProperty(String name) { public Property getProperty(String name) {
if ("key".equals(name)) { if ("key".equals(name)) {
try { try {
return new MethodProperty(new PropertyDescriptor("key", BasicAuth.class, "getKey", "setKey")); return new MethodProperty(new PropertyDescriptor("key", WithKey.class, "getKey", "setKey"));
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
if ("ca".equals(name)) {
try {
return new MethodProperty(new PropertyDescriptor("ca", TlsAuth.class, "getCa", "setCa"));
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
if ("certificate".equals(name)) {
try {
return new MethodProperty(new PropertyDescriptor("certificate", TlsAuth.class, "getCertificate", "setCertificate"));
} catch (IntrospectionException e) { } catch (IntrospectionException e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -293,22 +374,28 @@ public class UpstreamsConfig {
return super.getProperty(name); return super.getProperty(name);
} }
private Optional<ScalarNode> getValue(MappingNode mappingNode, String key) {
return mappingNode.getValue()
.stream()
.filter((n) -> n.getKeyNode() instanceof ScalarNode && n.getValueNode() instanceof ScalarNode)
.filter((n) -> {
ScalarNode sn = (ScalarNode)n.getKeyNode();
return "type".equals(sn.getValue());
})
.map((n) -> (ScalarNode)n.getValueNode())
.findFirst();
}
@Override @Override
public Object newInstance(Node node) { public Object newInstance(Node node) {
if (node instanceof MappingNode) { if (node instanceof MappingNode) {
MappingNode mappingNode = (MappingNode)node; MappingNode mappingNode = (MappingNode)node;
Optional<ScalarNode> type = mappingNode.getValue() Optional<ScalarNode> type = getValue(mappingNode, "type");
.stream()
.filter((n) -> n.getKeyNode() instanceof ScalarNode && n.getValueNode() instanceof ScalarNode)
.filter((n) -> {
ScalarNode sn = (ScalarNode)n.getKeyNode();
return "type".equals(sn.getValue());
})
.map((n) -> (ScalarNode)n.getValueNode())
.findFirst();
if (type.isPresent()) { if (type.isPresent()) {
if ("basic".equals(type.get().getValue())) { if ("basic".equals(type.get().getValue())) {
return new BasicAuth(); return new BasicAuth();
} else if ("tls".equals(type.get().getValue())) {
return new TlsAuth();
} else { } else {
throw new IllegalArgumentException("Unsupported auth type: " + type.get().getValue()); throw new IllegalArgumentException("Unsupported auth type: " + type.get().getValue());
} }

View File

@@ -16,7 +16,9 @@ class BlockchainRpc(
@Autowired private val nativeCall: NativeCall, @Autowired private val nativeCall: NativeCall,
@Autowired private val streamHead: StreamHead, @Autowired private val streamHead: StreamHead,
@Autowired private val trackTx: TrackTx, @Autowired private val trackTx: TrackTx,
@Autowired private val trackAddress: TrackAddress @Autowired private val trackAddress: TrackAddress,
@Autowired private val describe: Describe,
@Autowired private val subscribeStatus: SubscribeStatus
): BlockchainGrpc.BlockchainImplBase() { ): BlockchainGrpc.BlockchainImplBase() {
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java) private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
@@ -56,4 +58,12 @@ class BlockchainRpc(
} }
} }
override fun describe(request: BlockchainOuterClass.DescribeRequest, responseObserver: StreamObserver<BlockchainOuterClass.DescribeResponse>) {
describe.describe(request, responseObserver)
}
override fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
subscribeStatus.subscribeStatus(request, responseObserver)
}
} }

View File

@@ -0,0 +1,43 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.Upstreams
import io.grpc.stub.StreamObserver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class Describe(
@Autowired private val upstreams: Upstreams
) {
fun describe(request: BlockchainOuterClass.DescribeRequest, responseObserver: StreamObserver<BlockchainOuterClass.DescribeResponse>) {
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
upstreams.getAvailable().forEach { chain ->
upstreams.ethereumUpstream(chain).let { chainUpstreams ->
val quorum = chainUpstreams.getAll().map { u ->
if (u.getStatus() == UpstreamAvailability.OK) {
u.getOptions().quorum
} else {
0
}
}.sum()
val available = chainUpstreams.getAll().any { u ->
u.getStatus() == UpstreamAvailability.OK
}
resp.addChains(
BlockchainOuterClass.DescribeChain.newBuilder()
.setChain(Common.ChainRef.forNumber(chain.id))
.setQuorum(quorum)
.setAvailable(available)
.build()
)
}
}
responseObserver.onNext(resp.build())
responseObserver.onCompleted()
}
}

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.rpc
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver import io.grpc.stub.StreamObserver
@@ -27,7 +28,7 @@ class NativeCall(
if (chain == Chain.UNSPECIFIED) { if (chain == Chain.UNSPECIFIED) {
throw Exception("Invalid chain id: ${request.chain.number}") throw Exception("Invalid chain id: ${request.chain.number}")
} }
val upstream = upstreams.ethereumUpstream(chain)?.api ?: throw Exception("Chain ${chain.id} is unavailable") val upstream = upstreams.ethereumUpstream(chain)?.getApi() ?: throw Exception("Chain ${chain.id} is unavailable")
request.itemsList.toFlux() request.itemsList.toFlux()
.map { .map {
val method = it.target val method = it.target

View File

@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver import io.grpc.stub.StreamObserver
@@ -12,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.toFlux import reactor.core.publisher.toFlux
import java.lang.Exception import java.lang.Exception
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import kotlin.collections.HashMap import kotlin.collections.HashMap
@@ -28,7 +28,7 @@ class StreamHead(
@PostConstruct @PostConstruct
fun init() { fun init() {
listOf(Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_MORDEN, Chain.TESTNET_KOVAN).forEach { chain -> listOf(Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_MORDEN, Chain.TESTNET_KOVAN).forEach { chain ->
if (upstreams.ethereumUpstream(chain)?.head != null) { if (upstreams.ethereumUpstream(chain)?.getHead() != null) {
clients[chain] = ConcurrentLinkedQueue() clients[chain] = ConcurrentLinkedQueue()
subscribe(chain) subscribe(chain)
} }
@@ -36,7 +36,7 @@ class StreamHead(
} }
private fun subscribe(chain: Chain) { private fun subscribe(chain: Chain) {
upstreams.ethereumUpstream(chain)!!.head.getFlux() upstreams.ethereumUpstream(chain)!!.getHead().getFlux()
.doOnComplete { .doOnComplete {
log.info("Closing streams for ${chain.chainCode}") log.info("Closing streams for ${chain.chainCode}")
clients.replace(chain, ConcurrentLinkedQueue())!!.forEach { client -> clients.replace(chain, ConcurrentLinkedQueue())!!.forEach { client ->
@@ -68,7 +68,7 @@ class StreamHead(
fun process(chain: Chain, client: StreamSender<BlockchainOuterClass.ChainHead>): Boolean { fun process(chain: Chain, client: StreamSender<BlockchainOuterClass.ChainHead>): Boolean {
val upstream = upstreams.ethereumUpstream(chain) ?: return false val upstream = upstreams.ethereumUpstream(chain) ?: return false
val head = upstream.head.getHead() val head = upstream.getHead().getHead()
return head.map { return head.map {
notify(chain, it, client) notify(chain, it, client)
}.defaultIfEmpty(false).block()!! }.defaultIfEmpty(false).block()!!

View File

@@ -0,0 +1,19 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.Upstreams
import io.grpc.stub.StreamObserver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class SubscribeStatus(
@Autowired private val upstreams: Upstreams
) {
fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
upstreams.getAvailable().forEach { chain ->
}
}
}

View File

@@ -2,12 +2,12 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.domain.Address import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei import io.infinitape.etherjar.domain.Wei
import io.infinitape.etherjar.rpc.Batch
import io.infinitape.etherjar.rpc.Commands import io.infinitape.etherjar.rpc.Commands
import io.infinitape.etherjar.rpc.json.BlockTag import io.infinitape.etherjar.rpc.json.BlockTag
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -17,14 +17,11 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux import reactor.core.publisher.toFlux
import reactor.math.sum import reactor.math.sum
import reactor.util.function.Tuple2
import reactor.util.function.Tuples
import java.lang.Exception import java.lang.Exception
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
@Service @Service
@@ -40,7 +37,7 @@ class TrackAddress(
fun init() { fun init() {
allChains.forEach { chain -> allChains.forEach { chain ->
clients[chain] = ConcurrentLinkedQueue() clients[chain] = ConcurrentLinkedQueue()
upstreams.ethereumUpstream(chain)?.head?.let { head -> upstreams.ethereumUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) } head.getFlux().subscribe { verifyAll(chain) }
} }
} }
@@ -120,25 +117,18 @@ class TrackAddress(
} }
private fun verify(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> { private fun verify(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
val up = upstreams.ethereumUpstream(chain)!! val up = upstreams.ethereumUpstream(chain)
return group.toFlux() return group.toFlux()
.reduce<Tuple2<Batch, ArrayList<Update>>>(Tuples.of(Batch(), ArrayList())) { batch, a -> .flatMap { a ->
val f = batch.t1.add(Commands.eth().getBalance(a.address, BlockTag.LATEST)); up.getApi()
batch.t2.add(Update(a, f)) .executeAndConvert(Commands.eth().getBalance(a.address, BlockTag.LATEST))
batch .map { Update(a, it) }
}
.flatMap {
Mono.fromCompletionStage(up.api.execute(it.t1))
.thenReturn(it.t2)
}
.flatMapMany {
it.toFlux()
} }
.filter { .filter {
it.addr.balance == null || it.addr.balance != it.value.get() it.addr.balance == null || it.addr.balance != it.value
} }
.doOnNext { .doOnNext {
it.addr.balance = it.value.get() it.addr.balance = it.value
} }
.map { .map {
it.addr it.addr
@@ -163,7 +153,7 @@ class TrackAddress(
return sent return sent
} }
class Update(val addr: TrackedAddress, val value: Future<Wei>) class Update(val addr: TrackedAddress, val value: Wei)
class TrackedAddress(val chain: Chain, class TrackedAddress(val chain: Chain,
val stream: StreamSender<BlockchainOuterClass.AddressBalance>, val stream: StreamSender<BlockchainOuterClass.AddressBalance>,

View File

@@ -3,19 +3,17 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.Upstreams import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.domain.BlockHash import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.Batch
import io.infinitape.etherjar.rpc.Commands import io.infinitape.etherjar.rpc.Commands
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux import reactor.core.publisher.toFlux
import reactor.kotlin.core.publisher.switchIfEmpty
import java.lang.Exception import java.lang.Exception
import java.math.BigInteger import java.math.BigInteger
import java.time.Duration import java.time.Duration
@@ -37,7 +35,7 @@ class TrackTx(
fun init() { fun init() {
listOf(Chain.TESTNET_MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM, Chain.TESTNET_KOVAN).forEach { chain -> listOf(Chain.TESTNET_MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM, Chain.TESTNET_KOVAN).forEach { chain ->
clients[chain] = ConcurrentLinkedQueue() clients[chain] = ConcurrentLinkedQueue()
upstreams.ethereumUpstream(chain)?.head?.let { head -> upstreams.ethereumUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) } head.getFlux().subscribe { verifyAll(chain) }
} }
} }
@@ -63,9 +61,9 @@ class TrackTx(
} }
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> { private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
val batch = Batch() val upstream = upstreams.ethereumUpstream(tx.chain)
val execution = Mono return upstream.getApi()
.fromCompletionStage(batch.add(Commands.eth().getBlock(tx.status.blockHash))) .executeAndConvert(Commands.eth().getBlock(tx.status.blockHash))
.map { block -> .map { block ->
if (block != null && block.number != null && block.totalDifficulty != null) { if (block != null && block.number != null && block.totalDifficulty != null) {
tx.withStatus( tx.withStatus(
@@ -78,18 +76,14 @@ class TrackTx(
) )
} }
} }
val upstream = upstreams.ethereumUpstream(tx.chain)!!
upstream.api.execute(batch)
return execution
} }
private fun verify(tx: TrackedTx): Boolean { private fun verify(tx: TrackedTx): Boolean {
val found = tx.status.found val found = tx.status.found
val mined = tx.status.mined val mined = tx.status.mined
val batch = Batch() val upstream = upstreams.ethereumUpstream(tx.chain)
val execution = Mono.fromCompletionStage(batch.add(Commands.eth().getTransaction(tx.txid))) val execution = upstream.getApi()
val upstream = upstreams.ethereumUpstream(tx.chain)!! .executeAndConvert(Commands.eth().getTransaction(tx.txid))
upstream.api.execute(batch)
val update = execution.flatMap { val update = execution.flatMap {
if (it.blockNumber != null if (it.blockNumber != null
&& it.blockHash != null && it.blockHash != ZERO_BLOCK) { && it.blockHash != null && it.blockHash != ZERO_BLOCK) {
@@ -100,7 +94,7 @@ class TrackTx(
mined = true, mined = true,
confirmation = 1 confirmation = 1
) )
return@flatMap upstream.head.getHead().map { head -> return@flatMap upstream.getHead().getHead().map { head ->
tx.withStatus( tx.withStatus(
confirmation = head.number - tx.status.height!! + 1 confirmation = head.number - tx.status.height!! + 1
) )

View File

@@ -0,0 +1,52 @@
package io.emeraldpay.dshackle.upstream
abstract class AggregatedUpstreams {
abstract fun getAll(): List<Upstream>
abstract fun addUpstream(upstream: Upstream)
abstract fun getApis(quorum: Int): Iterator<EthereumApi>
abstract fun getApi(): EthereumApi
abstract fun getHead(): EthereumHead
class SingleApi(
private val quorumApi: QuorumApi
): Iterator<EthereumApi> {
private var consumed = false
override fun hasNext(): Boolean {
return !consumed && quorumApi.hasNext()
}
override fun next(): EthereumApi {
consumed = true
return quorumApi.next()
}
}
class QuorumApi(
private val apis: List<Upstream>,
private val quorum: Int,
private var pos: Int
): Iterator<EthereumApi> {
private var consumed = 0
override fun hasNext(): Boolean {
return consumed < quorum
}
override fun next(): EthereumApi {
val start = pos
while (pos < start + apis.size) {
val api = apis[pos++ % apis.size]
if (api.isAvailable()) {
consumed++
return api.getApi()
}
}
throw IllegalStateException("No upstream API available")
}
}
}

View File

@@ -1,93 +0,0 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson
import org.slf4j.LoggerFactory
import java.lang.IllegalStateException
import java.time.Duration
class ChainConnect(
val chain: Chain,
val upstreams: List<Upstream>
) {
private val log = LoggerFactory.getLogger(ChainConnect::class.java)
private var seq = 0
val head: EthereumHead = if (upstreams.size == 1) {
upstreams.first().head
} else {
EthereumHeadMerge(upstreams.map { it.head })
}
val api: EthereumApi
get() {
return getApis(1).next()
}
fun getApis(quorum: Int): Iterator<EthereumApi> {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
}
return QuorumApi(upstreams, 1, seq)
}
fun printStatus() {
var height: Long = -1
try {
height = head.getHead().block(Duration.ofSeconds(1))?.number ?: -1
} catch (e: Exception) { }
val statuses = upstreams.map { it.getStatus() }
.groupBy { it }
.map { "${it.key.name}/${it.value.size}" }
.joinToString(",")
log.info("State of ${chain.chainCode}: height=$height, status=$statuses")
}
class SingleApi(
private val quorumApi: QuorumApi
): Iterator<EthereumApi> {
private var consumed = false
override fun hasNext(): Boolean {
return !consumed && quorumApi.hasNext()
}
override fun next(): EthereumApi {
consumed = true
return quorumApi.next()
}
}
class QuorumApi(
private val apis: List<Upstream>,
private val quorum: Int,
private var pos: Int
): Iterator<EthereumApi> {
private var consumed = 0
override fun hasNext(): Boolean {
return consumed < quorum
}
override fun next(): EthereumApi {
val start = pos
while (pos < start + apis.size) {
val api = apis[pos++ % apis.size]
if (api.isAvailable()) {
consumed++
return api.api
}
}
throw IllegalStateException("No upstream API available")
}
}
}

View File

@@ -0,0 +1,72 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import java.lang.IllegalStateException
import java.time.Duration
class ChainUpstreams (
val chain: Chain,
private val upstreams: MutableList<Upstream>
) : AggregatedUpstreams() {
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
private var seq = 0
private var head: EthereumHead
init {
head = updateHead()
}
internal fun updateHead(): EthereumHead {
return if (upstreams.size == 1) {
upstreams.first().getHead()
} else {
EthereumHeadMerge(upstreams.map { it.getHead() })
}
}
override fun getAll(): List<Upstream> {
return upstreams
}
override fun addUpstream(upstream: Upstream) {
upstreams.add(upstream)
head = updateHead()
}
override fun getApis(quorum: Int): Iterator<EthereumApi> {
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
}
return QuorumApi(upstreams, 1, seq)
}
override fun getApi(): EthereumApi {
return getApis(1).next()
}
override fun getHead(): EthereumHead {
return head
}
fun printStatus() {
var height: Long = -1
try {
height = head.getHead().block(Duration.ofSeconds(1))?.number ?: -1
} catch (e: IllegalStateException) {
//timout
} catch (e: Exception) {
log.warn("Head processing error: ${e.javaClass} ${e.message}")
}
val statuses = upstreams.map { it.getStatus() }
.groupBy { it }
.map { "${it.key.name}/${it.value.size}" }
.joinToString(",")
log.info("State of ${chain.chainCode}: height=$height, status=$statuses")
}
}

View File

@@ -0,0 +1,171 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.DefaultRpcClient
import io.infinitape.etherjar.rpc.transport.DefaultRpcTransport
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Repository
import reactor.core.publisher.toFlux
import java.io.File
import java.net.URI
import java.util.*
import javax.annotation.PostConstruct
@Repository
open class ConfiguredUpstreams(
@Autowired val env: Environment,
@Autowired private val objectMapper: ObjectMapper
) : Upstreams {
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
private val chainMapping = HashMap<Chain, ChainUpstreams>()
private val chainNames = mapOf(
"ethereum" to Chain.ETHEREUM,
"ethereum-classic" to Chain.ETHEREUM_CLASSIC,
"eth" to Chain.ETHEREUM,
"etc" to Chain.ETHEREUM_CLASSIC,
"morden" to Chain.TESTNET_MORDEN,
"kovan" to Chain.TESTNET_KOVAN
)
@PostConstruct
fun start() {
val config = readConfig()
val defaultOptions = buildDefaultOptions(config)
val groups = HashMap<Chain, ArrayList<Upstream>>()
config.upstreams.forEach { up ->
if (up.provider == "dshackle") {
buildGrpcUpstream(up)
} else {
buildEthereumUpstream(up, defaultOptions, groups)
}
}
groups.forEach { chain, group ->
chainMapping[chain] = ChainUpstreams(chain, group)
}
}
private fun readConfig(): UpstreamsConfig {
val path = env.getProperty("upstreams.config")
if (StringUtils.isEmpty(path)) {
log.error("Path to upstreams is not set (upstreams.config)")
System.exit(1)
}
val upstreamConfig = File(path!!)
val ok = upstreamConfig.exists() && upstreamConfig.isFile
if (!ok) {
log.error("Unable to setup upstreams from ${upstreamConfig.path}")
System.exit(1)
}
log.info("Read upstream configuration from ${upstreamConfig.path}")
val reader = UpstreamsConfigReader()
return reader.read(upstreamConfig.inputStream())
}
private fun buildDefaultOptions(config: UpstreamsConfig): HashMap<Chain, UpstreamsConfig.Options> {
val defaultOptions = HashMap<Chain, UpstreamsConfig.Options>()
config.defaultOptions.forEach { df ->
df.chains.forEach { chainName ->
chainNames[chainName]?.let { chain ->
var current = defaultOptions[chain]
if (current == null) {
current = df.options
} else {
current = current.merge(df.options)
}
defaultOptions[chain] = current
}
}
}
return defaultOptions
}
private fun buildEthereumUpstream(up: UpstreamsConfig.Upstream,
defaultOptions: HashMap<Chain, UpstreamsConfig.Options>,
groups: HashMap<Chain, ArrayList<Upstream>>) {
val chain = chainNames[up.chain] ?: return
var rpcApi: EthereumApi? = null
var wsApi: EthereumWs? = null
val urls = ArrayList<URI>()
up.endpoints.forEach { endpoint ->
if (endpoint.type == UpstreamsConfig.EndpointType.JSON_RPC) {
rpcApi = EthereumApi(
DefaultRpcClient(DefaultRpcTransport(endpoint.url)),
objectMapper,
chain
)
}
if (endpoint.type == UpstreamsConfig.EndpointType.WEBSOCKET) {
wsApi = EthereumWs(
endpoint.url,
endpoint.origin ?: URI("http://localhost")
)
wsApi!!.connect()
}
urls.add(endpoint.url)
}
val options = (up.options ?: UpstreamsConfig.Options())
.merge(defaultOptions[chain])
.merge(UpstreamsConfig.Options.getDefaults())
if (rpcApi != null) {
log.info("Using ${chain.chainName} upstream, at ${urls.joinToString()}")
val current = groups[chain] ?: ArrayList()
current.add(EthereumUpstream(chain, rpcApi!!, wsApi, options))
groups[chain] = current
}
}
private fun buildGrpcUpstream(up: UpstreamsConfig.Upstream) {
if (up.endpoints.size == 0) {
return
}
val options = (up.options ?: UpstreamsConfig.Options())
.merge(UpstreamsConfig.Options.getDefaults())
val endpoint = up.endpoints.first()
if (endpoint.type == UpstreamsConfig.EndpointType.DSHACKLE) {
val ds = GrpcUpstreams(
endpoint.host,
endpoint.port ?: 443,
objectMapper,
options
)
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
ds.start()
.flatMapMany {
it.toFlux()
}
.subscribe {
log.info("Subscribed to $it through gRPC at ${endpoint.host}:${endpoint.port}")
ethereumUpstream(it).addUpstream(ds.getOrCreate(it))
}
}
}
override fun ethereumUpstream(chain: Chain): ChainUpstreams {
val current = chainMapping[chain]
if (current == null) {
val created = ChainUpstreams(chain, ArrayList<Upstream>())
chainMapping[chain] = created
return created
}
return current
}
@Scheduled(fixedRate = 15000)
fun printStatuses() {
chainMapping.forEach { it.value.printStatus() }
}
override fun getAvailable(): List<Chain> {
return Collections.unmodifiableList(chainMapping.keys.toList())
}
}

View File

@@ -3,24 +3,25 @@ package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.hex.HexQuantity import io.infinitape.etherjar.hex.HexQuantity
import io.infinitape.etherjar.rpc.Batch import io.infinitape.etherjar.rpc.*
import io.infinitape.etherjar.rpc.RpcCall
import io.infinitape.etherjar.rpc.RpcClient
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.json.ResponseJson import io.infinitape.etherjar.rpc.json.ResponseJson
import io.infinitape.etherjar.rpc.transport.BatchStatus import io.infinitape.etherjar.rpc.transport.BatchStatus
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class EthereumApi( open class EthereumApi(
private val rpcClient: RpcClient, val rpcClient: RpcClient,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val chain: Chain private val chain: Chain
) { ) {
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
private val timeout = Duration.ofSeconds(5) private val timeout = Duration.ofSeconds(5)
private val log = LoggerFactory.getLogger(EthereumApi::class.java) private val log = LoggerFactory.getLogger(EthereumApi::class.java)
var ws: EthereumWs? = null var ws: EthereumWs? = null
@@ -65,13 +66,19 @@ class EthereumApi(
"eth_accounts" "eth_accounts"
) )
fun execute(batch: Batch): CompletableFuture<BatchStatus> { open fun <JS, RS> executeAndConvert(rpcCall: RpcCall<JS, RS>): Mono<RS> {
return rpcClient.execute(batch) return execute(0, rpcCall.method, rpcCall.params as List<Any>)
.map {
jacksonRpcConverter.fromJson(it.inputStream(), rpcCall.jsonType, Int::class.java)
}.map {
rpcCall.converter.apply(it)
}
} }
fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> { open fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
val result: Mono<Any> = if (hardcodedMethods.contains(method)) { val result: Mono<Any> = if (hardcodedMethods.contains(method)) {
Mono.just(method).map { hardcoded(it) } Mono.just(method)
.map { hardcoded(it) }
} else if (allowedMethods.contains(method)) { } else if (allowedMethods.contains(method)) {
callUpstream(method, params) callUpstream(method, params)
} else { } else {

View File

@@ -0,0 +1,102 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.Batch
import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.transport.BatchStatus
import io.infinitape.etherjar.rpc.transport.RpcTransport
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux
import reactor.util.function.Tuple3
import reactor.util.function.Tuples
import java.util.concurrent.CompletableFuture
import java.util.function.Function
class EthereumGrpcTransport(
private val chain: Chain,
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper
): RpcTransport {
private val chainRef = Common.ChainRef.forNumber(chain.id)
private val jacksonRpcConverter = JacksonRpcConverter(objectMapper)
override fun close() {
}
private fun replyProcessor(mapping: HashMap<Int, Batch.BatchItem<Any, Any>>): Function<BlockchainOuterClass.NativeCallReplyItem, Boolean> {
return Function { resp ->
val id = resp.id
val bi = mapping.remove(id)
if (bi != null) {
if (resp.succeed) {
try {
val rpcResp = jacksonRpcConverter.fromJson(resp.payload.toByteArray().inputStream(), bi.call.jsonType, Int::class.java)
bi.onComplete(rpcResp)
return@Function true
} catch (e: RpcException) {
bi.onError(e)
}
} else {
bi.onError(RpcException(-32603, resp.error.toString()))
}
}
false
}
}
private val sumStatus = { t: Tuple3<Int, Int, Int>, ok: Boolean ->
if (ok) Tuples.of(t.t1 + 1, t.t2, t.t3 + 1)
else Tuples.of(t.t1, t.t2 + 1, t.t3 + 1)
}
private val asStatus = Function<Tuple3<Int, Int, Int>, BatchStatus> {
BatchStatus.newBuilder()
.withSucceed(it.t1)
.withFailed(it.t2)
.withTotal(it.t3)
.build()
}
fun prepareMapping(items: List<Batch.BatchItem<out Any, out Any>>, req: BlockchainOuterClass.NativeCallRequest.Builder): HashMap<Int, Batch.BatchItem<Any, Any>> {
val mapping = HashMap<Int, Batch.BatchItem<Any, Any>>()
var seq: Int = 0
items.forEach { bi ->
val id = seq++
mapping[id] = bi as Batch.BatchItem<Any, Any>
val call = bi.call
val params = objectMapper.writeValueAsBytes(call.params)
val nativeCallItem = BlockchainOuterClass.NativeCallItem.newBuilder()
.setId(id)
.setMethod("POST")
.setTarget(call.method)
.setPayload(ByteString.copyFrom(params))
.build()
req.addItems(nativeCallItem)
}
return mapping
}
override fun execute(items: List<Batch.BatchItem<out Any, out Any>>): CompletableFuture<BatchStatus> {
val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(chainRef);
val mapping = prepareMapping(items, req)
return client.nativeCall(req.build())
.map(replyProcessor(mapping))
.reduce(Tuples.of(0, 0, 0), sumStatus)
.map(asStatus)
.doFinally {
mapping.values.forEach { bi ->
bi.onError(RpcException(-32603, "RPC response not received"))
}
}
.toFuture()
}
}

View File

@@ -25,13 +25,13 @@ class EthereumRpcHead(
.flatMap { .flatMap {
val batch = Batch() val batch = Batch()
val f = batch.add(Commands.eth().blockNumber) val f = batch.add(Commands.eth().blockNumber)
api.execute(batch) api.rpcClient.execute(batch)
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5)) Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5))
} }
.flatMap { .flatMap {
val batch = Batch() val batch = Batch()
val f = batch.add(Commands.eth().getBlock(it)) val f = batch.add(Commands.eth().getBlock(it))
api.execute(batch) api.rpcClient.execute(batch)
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5)) Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5))
} }
.onErrorContinue { err, _ -> .onErrorContinue { err, _ ->

View File

@@ -0,0 +1,56 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicReference
class EthereumUpstream(
val chain: Chain,
private val api: EthereumApi,
private val ethereumWs: EthereumWs? = null,
private val options: UpstreamsConfig.Options
): Upstream {
private val log = LoggerFactory.getLogger(EthereumUpstream::class.java)
private val head: EthereumHead = if (ethereumWs != null) {
EthereumWsHead(ethereumWs)
} else {
EthereumRpcHead(api).apply {
this.start()
}
}
private val validator = UpstreamValidator(this, options)
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
init {
log.info("Configured for ${chain.chainName}")
validator.start()
.subscribe {
status.set(it)
}
}
override fun isAvailable(): Boolean {
return status.get() == UpstreamAvailability.OK
}
override fun getStatus(): UpstreamAvailability {
return status.get()
}
override fun getHead(): EthereumHead {
return head
}
override fun getApi(): EthereumApi {
return api
}
override fun getOptions(): UpstreamsConfig.Options {
return options
}
}

View File

@@ -0,0 +1,143 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import com.salesforce.reactorgrpc.GrpcRetry
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.*
import io.infinitape.etherjar.rpc.json.BlockJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toMono
import java.math.BigInteger
import java.time.Duration
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Function
open class GrpcUpstream(
private val chain: Chain,
private val client: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val objectMapper: ObjectMapper,
private val options: UpstreamsConfig.Options
): Upstream {
constructor(chain: Chain, client: ReactorBlockchainGrpc.ReactorBlockchainStub, objectMapper: ObjectMapper)
: this(chain, client, objectMapper, UpstreamsConfig.Options.getDefaults())
private val log = LoggerFactory.getLogger(GrpcUpstream::class.java)
private val headBlock = AtomicReference<BlockJson<TransactionId>>(null)
private val streamBlocks: TopicProcessor<BlockJson<TransactionId>> = TopicProcessor.create()
private var status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
private val head = Head(this)
private val api: EthereumApi
init {
val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
val rpcClient = DefaultRpcClient(grpcTransport)
api = EthereumApi(rpcClient, objectMapper, chain)
}
open fun connect() {
val chainRef = Common.Chain.newBuilder()
.setTypeValue(chain.id)
.build()
.toMono()
val retry: Function<Flux<BlockchainOuterClass.ChainHead>, Flux<BlockchainOuterClass.ChainHead>> = Function {
status.set(UpstreamAvailability.UNAVAILABLE)
client.subscribeHead(chainRef)
}
val flux = client.subscribeHead(chainRef)
.compose(GrpcRetry.ManyToMany.retryAfter(retry, Duration.ofSeconds(5)))
subscribe(flux)
}
internal fun subscribe(flux: Flux<BlockchainOuterClass.ChainHead>) {
flux.map { value ->
val block = BlockJson<TransactionId>()
block.number = value.height
block.totalDifficulty = BigInteger(1, value.weight.toByteArray())
block.hash = BlockHash.from("0x"+value.blockId)
block
}
.filter { block ->
val curr = headBlock.get()
curr == null || curr.totalDifficulty < block.totalDifficulty
}
.doOnError { err ->
log.error("Head subscription error", err)
}
.subscribe { block ->
log.debug("New block ${block.number} on ${chain}")
headBlock.set(block)
streamBlocks.onNext(block)
status.set(UpstreamAvailability.OK)
}
}
fun init(conf: BlockchainOuterClass.DescribeChain) {
val available = conf.available
val quorum = conf.quorum
status.set(
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
)
}
fun onStatus(value: BlockchainOuterClass.ChainStatus) {
val available = value.available
val quorum = value.quorum
status.set(
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
)
}
// ------------------------------------------------------------------------------------------
override fun isAvailable(): Boolean {
return headBlock.get() != null
}
override fun getStatus(): UpstreamAvailability {
return status.get()
}
override fun getHead(): EthereumHead {
return head
}
override fun getApi(): EthereumApi {
return api
}
override fun getOptions(): UpstreamsConfig.Options {
return options
}
class Head(
val upstream: GrpcUpstream
): EthereumHead {
override fun getHead(): Mono<BlockJson<TransactionId>> {
val current = upstream.headBlock.get()
if (current != null) {
return Mono.just(current)
}
return Mono.from(upstream.streamBlocks)
}
override fun getFlux(): Flux<BlockJson<TransactionId>> {
return Flux.from(upstream.streamBlocks)
}
}
}

View File

@@ -0,0 +1,69 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.grpc.Chain
import io.grpc.ManagedChannelBuilder
import reactor.core.publisher.Mono
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class GrpcUpstreams(
private val host: String,
private val port: Int,
private val objectMapper: ObjectMapper,
private val options: UpstreamsConfig.Options
) {
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
private var known = HashMap<Chain, GrpcUpstream>()
private val lock = ReentrantLock()
fun start(): Mono<List<Chain>> {
val channel = ManagedChannelBuilder.forAddress(host, port)
.enableRetry()
channel.usePlaintext()
val client = ReactorBlockchainGrpc.newReactorStub(channel.build())
this.client = client
val loaded = client.describe(BlockchainOuterClass.DescribeRequest.newBuilder().build())
.map { value ->
val chains = ArrayList<Chain>()
value.chainsList.forEach { chainDetails ->
val chain = Chain.byId(chainDetails.chain.number)
if (chain != Chain.UNSPECIFIED) {
getOrCreate(chain)
.init(chainDetails)
chains.add(chain)
}
}
chains as List<Chain>
}
//TODO subscribe only after receiving details
client.subscribeStatus(BlockchainOuterClass.StatusRequest.newBuilder().build())
.subscribe { value ->
val chain = Chain.byId(value.chain.number)
if (chain != Chain.UNSPECIFIED) {
getOrCreate(chain).onStatus(value)
}
}
return loaded
}
fun getOrCreate(chain: Chain): GrpcUpstream {
lock.withLock {
val current = known[chain]
return if (current == null) {
val created = GrpcUpstream(chain, client!!, objectMapper, options)
known[chain] = created
created.connect()
created
} else {
current
}
}
}
}

View File

@@ -1,44 +1,11 @@
package io.emeraldpay.dshackle.upstream package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.grpc.Chain
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicReference
class Upstream( interface Upstream {
val chain: Chain, fun isAvailable(): Boolean
val api: EthereumApi, fun getStatus(): UpstreamAvailability
private val ethereumWs: EthereumWs? = null, fun getHead(): EthereumHead
private val options: UpstreamsConfig.Options fun getApi(): EthereumApi
) { fun getOptions(): UpstreamsConfig.Options
private val log = LoggerFactory.getLogger(Upstream::class.java)
val head: EthereumHead = if (ethereumWs != null) {
EthereumWsHead(ethereumWs)
} else {
EthereumRpcHead(api).apply {
this.start()
}
}
val validator = UpstreamValidator(this, options)
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
init {
log.info("Configured for ${chain.chainName}")
validator.start()
.subscribe {
status.set(it)
}
}
fun isAvailable(): Boolean {
return status.get() == UpstreamAvailability.OK
}
fun getStatus(): UpstreamAvailability {
return status.get()
}
} }

View File

@@ -8,7 +8,7 @@ import java.time.Duration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class UpstreamValidator( class UpstreamValidator(
private val upstream: Upstream, private val ethereumUpstream: EthereumUpstream,
private val options: UpstreamsConfig.Options private val options: UpstreamsConfig.Options
) { ) {
@@ -17,7 +17,7 @@ class UpstreamValidator(
val peerCount = batch.add(Commands.net().peerCount()) val peerCount = batch.add(Commands.net().peerCount())
val syncing = batch.add(Commands.eth().syncing()) val syncing = batch.add(Commands.eth().syncing())
try { try {
upstream.api.execute(batch).get(5, TimeUnit.SECONDS) ethereumUpstream.getApi().rpcClient.execute(batch).get(5, TimeUnit.SECONDS)
if (syncing.get().isSyncing) { if (syncing.get().isSyncing) {
return UpstreamAvailability.SYNCING return UpstreamAvailability.SYNCING
} }

View File

@@ -1,116 +1,8 @@
package io.emeraldpay.dshackle.upstream package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.DefaultRpcClient
import io.infinitape.etherjar.rpc.transport.DefaultRpcTransport
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Repository
import java.io.File
import java.net.URI
import javax.annotation.PostConstruct
@Repository interface Upstreams {
class Upstreams( fun ethereumUpstream(chain: Chain): AggregatedUpstreams
@Autowired val env: Environment, fun getAvailable(): List<Chain>
@Autowired private val objectMapper: ObjectMapper
) {
private val log = LoggerFactory.getLogger(Upstreams::class.java)
private val chainMapping = HashMap<Chain, ChainConnect>()
private val chainNames = mapOf(
"ethereum" to Chain.ETHEREUM,
"ethereum-classic" to Chain.ETHEREUM_CLASSIC,
"eth" to Chain.ETHEREUM,
"etc" to Chain.ETHEREUM_CLASSIC,
"morden" to Chain.TESTNET_MORDEN,
"kovan" to Chain.TESTNET_KOVAN
)
@PostConstruct
fun start() {
val path = env.getProperty("upstreams.config")
if (StringUtils.isEmpty(path)) {
log.error("Path to upstreams is not set (upstreams.config)")
System.exit(1)
}
val upstreamConfig = File(path)
val ok = upstreamConfig.exists() && upstreamConfig.isFile
if (!ok) {
log.error("Unable to setup upstreams from ${upstreamConfig.path}")
System.exit(1)
}
log.info("Read upstream configuration from ${upstreamConfig.path}")
val reader = UpstreamsConfigReader()
val config = reader.read(upstreamConfig.inputStream())
val groups = HashMap<Chain, ArrayList<Upstream>>()
val defaultOptions = HashMap<Chain, UpstreamsConfig.Options>()
config.defaultOptions.forEach { df ->
df.chains.forEach { chainName ->
chainNames[chainName]?.let { chain ->
var current = defaultOptions[chain]
if (current == null) {
current = df.options
} else {
current = current.merge(df.options)
}
defaultOptions[chain] = current
}
}
}
config.upstreams.forEach { up ->
val chain = chainNames[up.chain] ?: return@forEach
var rpcApi: EthereumApi? = null
var wsApi: EthereumWs? = null
val urls = ArrayList<URI>()
up.endpoints.forEach { endpoint ->
if (endpoint.type == UpstreamsConfig.EndpointType.JSON_RPC) {
rpcApi = EthereumApi(
DefaultRpcClient(DefaultRpcTransport(endpoint.url)),
objectMapper,
chain
)
}
if (endpoint.type == UpstreamsConfig.EndpointType.WEBSOCKET) {
wsApi = EthereumWs(
endpoint.url,
endpoint.origin ?: URI("http://localhost")
)
wsApi!!.connect()
}
urls.add(endpoint.url)
}
val options = (up.options ?: UpstreamsConfig.Options())
.merge(defaultOptions[chain])
.merge(UpstreamsConfig.Options.getDefaults())
if (rpcApi != null) {
log.info("Info using ${chain.chainName} upstream, at ${urls.joinToString()}")
val current = groups[chain] ?: ArrayList()
current.add(Upstream(chain, rpcApi!!, wsApi, options))
groups[chain] = current
}
}
groups.forEach { chain, group ->
chainMapping[chain] = ChainConnect(chain, group)
}
}
fun ethereumUpstream(chain: Chain): ChainConnect? {
return chainMapping[chain]
}
@Scheduled(fixedRate = 15000)
fun printStatuses() {
chainMapping.forEach { it.value.printStatus() }
}
} }

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="dfltPattern">%d{HH:mm:ss.SSS} [%-20t] %-5level %24.24c{1} | %msg%n</Property>
</Properties>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="${dfltPattern}"/>
<Filters>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Filters>
</Console>
<Console name="STDERR" target="SYSTEM_ERR">
<PatternLayout pattern="${dfltPattern}" />
</Console>
</Appenders>
<Loggers>
<Logger name="io.emeraldpay" level="debug" additivity="false">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="STDERR" level="warn"/>
</Logger>
<Root level="warn" additivity="false">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="STDERR" level="warn"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -54,4 +54,31 @@ class UpstreamsConfigReaderSpec extends Specification {
} }
} }
def "Parse ds config"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("upstreams-ds.yaml")
when:
def act = reader.read(config)
then:
act != null
act.version == "v1"
act.upstreams.size() == 1
with(act.upstreams.get(0)) {
id == "remote"
chain == "auto"
provider == "dshackle"
endpoints.size() == 1
with(endpoints.get(0)) {
type == UpstreamsConfig.EndpointType.DSHACKLE
host == "10.2.0.15"
auth instanceof UpstreamsConfig.TlsAuth
with((UpstreamsConfig.TlsAuth)auth) {
ca == "/etc/ca.myservice.com.crt"
certificate == "/etc/client1.myservice.com.crt"
key == "/etc/client1.myservice.com.key"
}
}
}
}
} }

View File

@@ -0,0 +1,54 @@
package io.emeraldpay.dshackle.test
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.EthereumApi
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.rpc.RpcClient
import io.infinitape.etherjar.rpc.RpcResponseError
import io.infinitape.etherjar.rpc.json.ResponseJson
import org.jetbrains.annotations.NotNull
import reactor.core.publisher.Mono
class EthereumApiMock extends EthereumApi {
List<PredefinedResponse> predefined = []
private ObjectMapper objectMapper
EthereumApiMock(@NotNull RpcClient rpcClient, @NotNull ObjectMapper objectMapper, @NotNull Chain chain) {
super(rpcClient, objectMapper, chain)
this.objectMapper = objectMapper
}
EthereumApiMock answer(@NotNull String method, List<Object> params, Object result) {
predefined << new PredefinedResponse(method: method, params: params, result: result)
return this
}
@Override
Mono<byte[]> execute(int id, @NotNull String method, @NotNull List<?> params) {
def predefined = predefined.find { it.isSame(id, method, params) }
ResponseJson json = new ResponseJson<Object, Integer>(id: id)
if (predefined != null) {
json.result = predefined.result
} else {
json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked")
}
return Mono.just(objectMapper.writeValueAsBytes(json))
}
class PredefinedResponse {
String method
List params
Object result
boolean isSame(int id, String method, List<?> params) {
if (method != this.method) {
return false
}
if (this.params == null) {
return true
}
return this.params == params
}
}
}

View File

@@ -0,0 +1,20 @@
package io.emeraldpay.dshackle.test
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.grpc.inprocess.InProcessChannelBuilder
import io.grpc.inprocess.InProcessServerBuilder
import io.grpc.testing.GrpcCleanupRule
class MockServer {
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule()
ReactorBlockchainGrpc.ReactorBlockchainStub runServer(BlockchainGrpc.BlockchainImplBase impl){
String serverName = InProcessServerBuilder.generateName()
grpcCleanup.register(InProcessServerBuilder
.forName(serverName).directExecutor().addService(impl).build().start());
def channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build())
return ReactorBlockchainGrpc.newReactorStub(channel)
}
}

View File

@@ -0,0 +1,24 @@
package io.emeraldpay.dshackle.test
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import java.text.SimpleDateFormat
class TestingCommons {
static ObjectMapper objectMapper() {
def module = new SimpleModule("EmeraldDShackle", new Version(1, 0, 0, null, null, null))
def objectMapper = new ObjectMapper()
objectMapper.registerModule(module)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSS"))
.setTimeZone(TimeZone.getTimeZone("UTC"))
return objectMapper
}
}

View File

@@ -0,0 +1,112 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.EthereumApiMock
import io.emeraldpay.dshackle.test.MockServer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.rpc.Batch
import io.infinitape.etherjar.rpc.RpcCall
import io.infinitape.etherjar.rpc.RpcClient
import spock.lang.Specification
class EthereumGrpcTransportSpec extends Specification {
MockServer mockServer = new MockServer()
ObjectMapper objectMapper = TestingCommons.objectMapper()
def "Make simple call"() {
setup:
def callData = [:]
def otherSideUpstreams = Mock(Upstreams)
def otherSideAggr = Mock(AggregatedUpstreams)
def otherSideNativeCall = new NativeCall(otherSideUpstreams, objectMapper)
def otherSideApi = new EthereumApiMock(Mock(RpcClient), objectMapper, Chain.ETHEREUM)
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
callData["request"] = request
otherSideNativeCall.nativeCall(request, responseObserver)
}
})
EthereumGrpcTransport transport = new EthereumGrpcTransport(Chain.ETHEREUM, client, objectMapper)
when:
otherSideApi.answer("eth_test", [1], "bar")
def batch = new Batch()
def f = batch.add(RpcCall.create("eth_test", [1]))
def status = transport.execute(batch.items).get()
then:
1 * otherSideUpstreams.ethereumUpstream(Chain.ETHEREUM) >> otherSideAggr
1 * otherSideAggr.api >> otherSideApi
status.failed == 0
status.succeed == 1
status.total == 1
callData.request != null
with((BlockchainOuterClass.NativeCallRequest)callData.request) {
chain.number == Chain.ETHEREUM.id
itemsCount == 1
with(getItems(0)) {
target == "eth_test"
payload.toStringUtf8() == "[1]"
}
}
f.get() == "bar"
}
def "Make few calls"() {
setup:
def callData = [:]
def otherSideUpstreams = Mock(Upstreams)
def otherSideAggr = Mock(AggregatedUpstreams)
def otherSideNativeCall = new NativeCall(otherSideUpstreams, objectMapper)
def otherSideApi = new EthereumApiMock(Mock(RpcClient), objectMapper, Chain.ETHEREUM)
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void nativeCall(BlockchainOuterClass.NativeCallRequest request, StreamObserver<BlockchainOuterClass.NativeCallReplyItem> responseObserver) {
callData["request"] = request
otherSideNativeCall.nativeCall(request, responseObserver)
}
})
EthereumGrpcTransport transport = new EthereumGrpcTransport(Chain.ETHEREUM, client, objectMapper)
when:
otherSideApi.answer("eth_test", [1], "bar")
otherSideApi.answer("eth_test2", [2, "3"], "baz")
def batch = new Batch()
def f1 = batch.add(RpcCall.create("eth_test", [1]))
def f2 = batch.add(RpcCall.create("eth_test2", [2, "3"]))
def status = transport.execute(batch.items).get()
then:
1 * otherSideUpstreams.ethereumUpstream(Chain.ETHEREUM) >> otherSideAggr
1 * otherSideAggr.api >> otherSideApi
status.failed == 0
status.succeed == 2
status.total == 2
callData.request != null
with((BlockchainOuterClass.NativeCallRequest)callData.request) {
chain.number == Chain.ETHEREUM.id
itemsCount == 2
with(getItems(0)) {
target == "eth_test"
payload.toStringUtf8() == "[1]"
}
with(getItems(1)) {
target == "eth_test2"
payload.toStringUtf8() == "[2,\"3\"]"
}
}
f1.get() == "bar"
f2.get() == "baz"
}
}

View File

@@ -0,0 +1,121 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.test.MockServer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.domain.BlockHash
import org.apache.commons.codec.binary.Hex
import spock.lang.Specification
import java.util.concurrent.CompletableFuture
class GrpcUpstreamSpec extends Specification {
MockServer mockServer = new MockServer()
ObjectMapper objectMapper = TestingCommons.objectMapper()
def "Subscribe to head"() {
setup:
def callData = [:]
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
callData.chain = request.getTypeValue()
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId("50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
.setHeight(650246)
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6456")))
.build()
)
}
})
def chain = Chain.ETHEREUM
def upstream = new GrpcUpstream(chain, client, objectMapper)
when:
upstream.connect()
def h = upstream.head.head.block()
then:
callData.chain == Chain.ETHEREUM.id
upstream.status == UpstreamAvailability.OK
h.hash == BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
}
def "Follows difficulty, ignores less difficult"() {
setup:
def callData = [:]
def finished = new CompletableFuture<Boolean>()
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId("50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
.setHeight(650246)
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6456")))
.build()
)
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId("3ec2ebf5d0ec474d0ac6bca770d8409ad750d26e119968e7919f85d5ec891521")
.setHeight(650247)
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6455")))
.build()
)
finished.complete(true)
}
})
def chain = Chain.ETHEREUM
def upstream = new GrpcUpstream(chain, client, objectMapper)
when:
upstream.connect()
finished.get()
def h = upstream.head.head.block()
then:
upstream.status == UpstreamAvailability.OK
h.hash == BlockHash.from("0x50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
h.number == 650246
}
def "Follows difficulty"() {
setup:
def callData = [:]
def finished = new CompletableFuture<Boolean>()
def client = mockServer.runServer(new BlockchainGrpc.BlockchainImplBase() {
@Override
void subscribeHead(Common.Chain request, StreamObserver<BlockchainOuterClass.ChainHead> responseObserver) {
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId("50d26e119968e791970d84a7bf5d0ec474d3ec2ef85d5ec8915210ac6bc09ad7")
.setHeight(650246)
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6456")))
.build()
)
responseObserver.onNext(
BlockchainOuterClass.ChainHead.newBuilder()
.setBlockId("3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
.setHeight(650247)
.setWeight(ByteString.copyFrom(Hex.decodeHex("35bbde5595de6457")))
.build()
)
finished.complete(true)
}
})
def chain = Chain.ETHEREUM
def upstream = new GrpcUpstream(chain, client, objectMapper)
when:
upstream.connect()
finished.get()
def h = upstream.head.head.block()
then:
upstream.status == UpstreamAvailability.OK
h.hash == BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec891521a")
h.number == 650247
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="dfltPattern">%d{HH:mm:ss.SSS} [%-20t] %-5level %24.24c{1} | %msg%n</Property>
</Properties>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="${dfltPattern}"/>
<Filters>
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Filters>
</Console>
<Console name="STDERR" target="SYSTEM_ERR">
<PatternLayout pattern="${dfltPattern}" />
</Console>
</Appenders>
<Loggers>
<Logger name="io.emeraldpay" level="debug" additivity="false">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="STDERR" level="warn"/>
</Logger>
<Root level="warn" additivity="false">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="STDERR" level="warn"/>
</Root>
</Loggers>
</Configuration>