solution: read upstream config from specified yaml

This commit is contained in:
Igor Artamonov
2019-06-20 16:40:54 -04:00
parent 057c3a5a02
commit cd1acd13c3
6 changed files with 502 additions and 33 deletions

View File

@@ -0,0 +1,334 @@
package io.emeraldpay.dshackle.config;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.introspector.MethodProperty;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.introspector.PropertySubstitute;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.ScalarNode;
import javax.annotation.Nullable;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class Upstreams {
private String version;
private List<DefaultOptions> defaultOptions;
private List<Upstream> upstreams;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public List<DefaultOptions> getDefaultOptions() {
return defaultOptions;
}
public void setDefaultOptions(List<DefaultOptions> defaultOptions) {
this.defaultOptions = defaultOptions;
}
public List<Upstream> getUpstreams() {
return upstreams;
}
public void setUpstreams(List<Upstream> upstreams) {
this.upstreams = upstreams;
}
public static class Options {
private Boolean disableSyncing = true;
private Integer minPeers = 1;
private Integer quorum = 1;
public Boolean getDisableSyncing() {
return disableSyncing;
}
public void setDisableSyncing(Boolean disableSyncing) {
this.disableSyncing = disableSyncing;
}
public Integer getMinPeers() {
return minPeers;
}
public void setMinPeers(Integer minPeers) {
this.minPeers = minPeers;
}
public Integer getQuorum() {
return quorum;
}
public void setQuorum(Integer quorum) {
this.quorum = quorum;
}
}
public static class OptionsYaml extends TypeDescription {
public OptionsYaml() {
super(Options.class);
super.substituteProperty(new PropertySubstitute("disable-syncing", Boolean.class,
"getDisableSyncing", "setDisableSyncing"));
super.substituteProperty(new PropertySubstitute("min-peers", Integer.class,
"getMinPeers", "setMinPeers"));
}
}
public static class DefaultOptions extends Options {
private List<String> chains;
private Options options;
public List<String> getChains() {
return chains;
}
public void setChains(List<String> chains) {
this.chains = chains;
}
public Options getOptions() {
return options;
}
public void setOptions(Options options) {
this.options = options;
}
}
public static class Upstream {
private String id;
private String chain;
@Nullable
private String provider;
private List<Endpoint> endpoints = Collections.emptyList();
private Options options;
private boolean enabled = true;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getChain() {
return chain;
}
public void setChain(String chain) {
this.chain = chain;
}
@Nullable
public String getProvider() {
return provider;
}
public void setProvider(@Nullable String provider) {
this.provider = provider;
}
public List<Endpoint> getEndpoints() {
return endpoints;
}
public void setEndpoints(List<Endpoint> endpoints) {
this.endpoints = endpoints;
}
public Options getOptions() {
return options;
}
public void setOptions(Options options) {
this.options = options;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Endpoint {
private EndpointType type;
private URI url;
@Nullable
private Auth auth;
private Boolean enabled = true;
@Nullable
private URI origin;
public EndpointType getType() {
return type;
}
public void setType(EndpointType type) {
this.type = type;
}
public URI getUrl() {
return url;
}
public void setUrl(URI url) {
this.url = url;
}
@Nullable
public Auth getAuth() {
return auth;
}
public void setAuth(@Nullable Auth auth) {
this.auth = auth;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
@Nullable
public URI getOrigin() {
return origin;
}
public void setOrigin(@Nullable URI origin) {
this.origin = origin;
}
}
public static class Auth {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
public static class BasicAuth extends Auth {
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static class AuthYaml extends TypeDescription {
public AuthYaml() {
super(Auth.class);
}
@Override
public Property getProperty(String name) {
if ("key".equals(name)) {
try {
return new MethodProperty(new PropertyDescriptor("key", BasicAuth.class, "getKey", "setKey"));
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
return super.getProperty(name);
}
@Override
public Object newInstance(Node node) {
if (node instanceof MappingNode) {
MappingNode mappingNode = (MappingNode)node;
Optional<ScalarNode> type = 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();
if (type.isPresent()) {
if ("basic".equals(type.get().getValue())) {
return new BasicAuth();
} else {
throw new IllegalArgumentException("Unsupported auth type: " + type.get().getValue());
}
} else {
throw new IllegalArgumentException("Auth type is not set");
}
}
throw new IllegalArgumentException("Auth is invalid");
}
}
public static enum EndpointType {
JSON_RPC("json-rpc"),
WEBSOCKET("ws", "websocket"),
DSHACKLE("dshackle"),
UNKNOWN("unknown");
private final String[] code;
EndpointType(String ... code) {
this.code = code;
Arrays.sort(this.code);
}
public static EndpointType byName(String code) {
code = code.toLowerCase();
for (EndpointType t: EndpointType.values()) {
if (Arrays.binarySearch(t.code, code) >= 0) {
return t;
}
}
return UNKNOWN;
}
}
public static class EndpointTypeYaml extends TypeDescription {
public EndpointTypeYaml() {
super(EndpointType.class);
}
@Override
public Object newInstance(Node node) {
if (node instanceof ScalarNode) {
return EndpointType.byName(((ScalarNode)node).getValue());
}
throw new IllegalArgumentException("Invalid type: " + node.getClass());
}
}
}

View File

@@ -0,0 +1,4 @@
package io.emeraldpay.dshackle.config
class Configuration {
}

View File

@@ -0,0 +1,17 @@
package io.emeraldpay.dshackle.config
import org.yaml.snakeyaml.Yaml
import java.io.InputStream
class UpstreamsReader {
fun read(input: InputStream): Upstreams {
val yaml = Yaml()
yaml.addTypeDescription(Upstreams.EndpointTypeYaml())
yaml.addTypeDescription(Upstreams.OptionsYaml())
yaml.addTypeDescription(Upstreams.AuthYaml())
return yaml.loadAs(input, Upstreams::class.java)
}
}

View File

@@ -1,12 +1,16 @@
package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.UpstreamsReader
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.stereotype.Repository
import java.io.File
import java.net.URI
import javax.annotation.PostConstruct
@@ -16,48 +20,72 @@ class Upstreams(
@Autowired private val objectMapper: ObjectMapper
) {
private val log = LoggerFactory.getLogger(Upstreams::class.java)
private var seq = 0
private val chainMapping = HashMap<Chain, Upstream>()
private val chainMapping = HashMap<Chain, ArrayList<Upstream>>()
private val chainNames = mapOf(
"ethereum" to Chain.ETHEREUM,
"ethereum-classic" to Chain.ETHEREUM_CLASSIC,
"morden" to Chain.MORDEN
)
@PostConstruct
fun start() {
env.getProperty("upstream.ethereum")?.let {
val api = buildClient(it, Chain.ETHEREUM)
chainMapping[Chain.ETHEREUM] = Upstream(Chain.ETHEREUM, api)
val path = env.getProperty("upstreams.config")
if (StringUtils.isEmpty(path)) {
log.error("Path to upstreams is not set (upstreams.config)")
System.exit(1)
}
env.getProperty("upstream.ethereumclassic")?.let {
val api = buildClient(it, Chain.ETHEREUM_CLASSIC)
val ws = if (env.containsProperty("upstream.ethereumclassic.ws")) {
buildWs(env.getProperty("upstream.ethereumclassic.ws")!!, Chain.ETHEREUM_CLASSIC)
} else {
null
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 = UpstreamsReader()
val config = reader.read(upstreamConfig.inputStream())
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 == io.emeraldpay.dshackle.config.Upstreams.EndpointType.JSON_RPC) {
rpcApi = EthereumApi(
DefaultRpcClient(DefaultRpcTransport(endpoint.url)),
objectMapper,
chain
)
}
if (endpoint.type == io.emeraldpay.dshackle.config.Upstreams.EndpointType.WEBSOCKET) {
wsApi = EthereumWs(
endpoint.url,
endpoint.origin ?: URI("http://localhost")
)
}
urls.add(endpoint.url)
}
if (rpcApi != null) {
log.info("Info using ${chain.chainName} upstream, at ${urls.joinToString()}")
val current = chainMapping[chain] ?: ArrayList()
current.add(Upstream(chain, rpcApi!!, wsApi))
chainMapping[chain] = current
}
chainMapping[Chain.ETHEREUM_CLASSIC] = Upstream(Chain.ETHEREUM_CLASSIC, api, ws)
}
env.getProperty("upstream.morden")?.let {
val api = buildClient(it, Chain.MORDEN)
chainMapping[Chain.MORDEN] = Upstream(Chain.MORDEN, api)
}
}
private fun buildClient(url: String, chain: Chain): EthereumApi {
return EthereumApi(
DefaultRpcClient(DefaultRpcTransport(URI(url))),
objectMapper,
chain
)
}
private fun buildWs(url: String, chain: Chain): EthereumWs {
val ws = EthereumWs(
URI(url),
URI("http://localhost")
)
ws.connect()
return ws
}
fun ethereumUpstream(chain: Chain): Upstream? {
return chainMapping[chain]
val list = chainMapping[chain]
if (list == null || list.isEmpty()) {
return null
}
val i = seq++
if (seq >= Int.MAX_VALUE / 2) {
seq = 0
}
return list.get(i % list.size)
}
}

View File

@@ -0,0 +1,57 @@
package io.emeraldpay.dshackle.config
import spock.lang.Specification
class UpstreamsReaderSpec extends Specification {
UpstreamsReader reader = new UpstreamsReader()
def "Parse standard config"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("upstreams-basic.yaml")
when:
def act = reader.read(config)
then:
act != null
act.version == "v1"
with(act.defaultOptions) {
size() == 1
with(get(0)) {
chains == ["ethereum"]
options.quorum == 1
options.minPeers == 3
options.disableSyncing
}
}
act.upstreams.size() == 2
with(act.upstreams.get(0)) {
id == "local"
chain == "ethereum"
provider == "geth"
endpoints.size() == 2
with(endpoints.get(0)) {
type == Upstreams.EndpointType.JSON_RPC
url == new URI("http://localhost:8545")
}
with(endpoints.get(1)) {
type == Upstreams.EndpointType.WEBSOCKET
url == new URI("ws://localhost:8546")
}
}
with(act.upstreams.get(1)) {
id == "infura"
chain == "ethereum"
provider == "infura"
endpoints.size() == 1
with(endpoints.get(0)) {
type == Upstreams.EndpointType.JSON_RPC
url == new URI("https://mainnet.infura.io/v3/fa28c968191849c1aff541ad1d8511f2")
auth instanceof Upstreams.BasicAuth
with((Upstreams.BasicAuth)auth) {
key == "4fc258fe41a68149c199ad8f281f2015"
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
version: v1
defaultOptions:
- chains:
- ethereum
options:
disable-syncing: true
min-peers: 3
quorum: 1
upstreams:
- id: local
chain: ethereum
provider: geth
endpoints:
- type: json-rpc
url: "http://localhost:8545"
- type: ws
url: "ws://localhost:8546"
origin: "http://localhost"
- id: infura
chain: ethereum
provider: infura
endpoints:
- type: json-rpc
url: "https://mainnet.infura.io/v3/fa28c968191849c1aff541ad1d8511f2"
auth:
type: basic
key: 4fc258fe41a68149c199ad8f281f2015