Coverage Summary for Class: SystemProperties (org.ethereum.config)

Class Class, % Method, % Line, %
SystemProperties 100% (1/1) 17.4% (15/86) 18.3% (42/229)


1 /* 2  * This file is part of RskJ 3  * Copyright (C) 2017 RSK Labs Ltd. 4  * (derived from ethereumJ library, Copyright (c) 2016 <ether.camp>) 5  * 6  * This program is free software: you can redistribute it and/or modify 7  * it under the terms of the GNU Lesser General Public License as published by 8  * the Free Software Foundation, either version 3 of the License, or 9  * (at your option) any later version. 10  * 11  * This program is distributed in the hope that it will be useful, 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14  * GNU Lesser General Public License for more details. 15  * 16  * You should have received a copy of the GNU Lesser General Public License 17  * along with this program. If not, see <http://www.gnu.org/licenses/>. 18  */ 19  20 package org.ethereum.config; 21  22 import co.rsk.bitcoinj.core.BtcECKey; 23 import co.rsk.config.BridgeDevNetConstants; 24 import co.rsk.config.BridgeRegTestConstants; 25 import co.rsk.config.ConfigLoader; 26 import com.typesafe.config.Config; 27 import com.typesafe.config.ConfigObject; 28 import com.typesafe.config.ConfigRenderOptions; 29 import org.bouncycastle.util.encoders.Hex; 30 import org.ethereum.config.blockchain.upgrades.ActivationConfig; 31 import org.ethereum.crypto.ECKey; 32 import org.ethereum.crypto.Keccak256Helper; 33 import org.ethereum.net.p2p.P2pHandler; 34 import org.ethereum.net.rlpx.MessageCodec; 35 import org.ethereum.net.rlpx.Node; 36 import org.ethereum.util.ByteUtil; 37 import org.slf4j.Logger; 38 import org.slf4j.LoggerFactory; 39  40 import java.io.*; 41 import java.net.InetAddress; 42 import java.net.MalformedURLException; 43 import java.net.URL; 44 import java.net.UnknownHostException; 45 import java.nio.charset.StandardCharsets; 46 import java.util.*; 47 import java.util.concurrent.TimeUnit; 48 import java.util.stream.Collectors; 49  50 /** 51  * Utility class to retrieve property values from the rskj.conf files 52  * <p> 53  * The properties are taken from different sources and merged in the following order 54  * (the config option from the next source overrides option from previous): 55  * - resource rskj.conf : normally used as a reference config with default values 56  * and shouldn't be changed 57  * - system property : each config entry might be altered via -D VM option 58  * - [user dir]/config/rskj.conf 59  * - config specified with the -Drsk.conf.file=[file.conf] VM option 60  * - CLI options 61  * 62  * @author Roman Mandeleil 63  * @since 22.05.2014 64  */ 65 public abstract class SystemProperties { 66  private static Logger logger = LoggerFactory.getLogger("general"); 67  68  public static final String PROPERTY_BLOCKCHAIN_CONFIG = "blockchain.config"; 69  public static final String PROPERTY_BC_CONFIG_NAME = PROPERTY_BLOCKCHAIN_CONFIG + ".name"; 70  public static final String PROPERTY_BC_VERIFY = PROPERTY_BLOCKCHAIN_CONFIG + ".verify"; 71  public static final String PROPERTY_GENESIS_CONSTANTS_FEDERATION_PUBLICKEYS = "genesis_constants.federationPublicKeys"; 72  public static final String PROPERTY_PEER_PORT = "peer.port"; 73  public static final String PROPERTY_BASE_PATH = "database.dir"; 74  public static final String PROPERTY_DB_RESET = "database.reset"; 75  public static final String PROPERTY_DB_IMPORT = "database.import.enabled"; 76  // TODO review rpc properties 77  public static final String PROPERTY_RPC_CORS = "rpc.providers.web.cors"; 78  public static final String PROPERTY_RPC_HTTP_ENABLED = "rpc.providers.web.http.enabled"; 79  public static final String PROPERTY_RPC_HTTP_ADDRESS = "rpc.providers.web.http.bind_address"; 80  public static final String PROPERTY_RPC_HTTP_HOSTS = "rpc.providers.web.http.hosts"; 81  public static final String PROPERTY_RPC_HTTP_PORT = "rpc.providers.web.http.port"; 82  private static final String PROPERTY_RPC_WEBSOCKET_ENABLED = "rpc.providers.web.ws.enabled"; 83  private static final String PROPERTY_RPC_WEBSOCKET_ADDRESS = "rpc.providers.web.ws.bind_address"; 84  private static final String PROPERTY_RPC_WEBSOCKET_PORT = "rpc.providers.web.ws.port"; 85  86  public static final String PROPERTY_PUBLIC_IP = "public.ip"; 87  public static final String PROPERTY_BIND_ADDRESS = "bind_address"; 88  89  public static final String PROPERTY_PRINT_SYSTEM_INFO = "system.printInfo"; 90  91  public static final String PROPERTY_SKIP_JAVA_VERSION_CHECK = "system.checkJavaVersion"; 92  93  /* Testing */ 94  private static final Boolean DEFAULT_VMTEST_LOAD_LOCAL = false; 95  96  protected final Config configFromFiles; 97  98  // mutable options for tests 99  private String databaseDir = null; 100  private String projectVersion = null; 101  private String projectVersionModifier = null; 102  103  private String genesisInfo = null; 104  105  private String publicIp = null; 106  107  private ActivationConfig activationConfig; 108  private Constants constants; 109  110  protected SystemProperties(ConfigLoader loader) { 111  try { 112  this.configFromFiles = loader.getConfig(); 113  logger.trace( 114  "Config trace: {}", 115  configFromFiles.root().render(ConfigRenderOptions.defaults().setComments(false).setJson(false)) 116  ); 117  118  Properties props = new Properties(); 119  try (InputStream is = getClass().getResourceAsStream("/version.properties")) { 120  props.load(is); 121  } 122  this.projectVersion = getProjectVersion(props); 123  this.projectVersionModifier = getProjectVersionModifier(props); 124  125  } catch (Exception e) { 126  logger.error("Can't read config.", e); 127  throw new RuntimeException(e); 128  } 129  } 130  131  private static String getProjectVersion(Properties props) { 132  String versionNumber = props.getProperty("versionNumber"); 133  134  if (versionNumber == null) { 135  return "-.-.-"; 136  } 137  138  return versionNumber.replaceAll("'", ""); 139  } 140  141  private static String getProjectVersionModifier(Properties props) { 142  return props.getProperty("modifier").replaceAll("\"", ""); 143  } 144  145  public Config getConfig() { 146  return configFromFiles; 147  } 148  149  public ActivationConfig getActivationConfig() { 150  if (activationConfig == null) { 151  activationConfig = ActivationConfig.read(configFromFiles.getConfig(PROPERTY_BLOCKCHAIN_CONFIG)); 152  } 153  154  return activationConfig; 155  } 156  157  public Constants getNetworkConstants() { 158  if (constants == null) { 159  switch (netName()) { 160  case "main": 161  constants = Constants.mainnet(); 162  break; 163  case "testnet": 164  constants = Constants.testnet(); 165  break; 166  case "devnet": 167  constants = Constants.devnetWithFederation( 168  getGenesisFederationPublicKeys().orElse(BridgeDevNetConstants.DEVNET_FEDERATION_PUBLIC_KEYS) 169  ); 170  break; 171  case "regtest": 172  constants = Constants.regtestWithFederation( 173  getGenesisFederationPublicKeys().orElse(BridgeRegTestConstants.REGTEST_FEDERATION_PUBLIC_KEYS) 174  ); 175  break; 176  default: 177  throw new RuntimeException(String.format("Unknown network name '%s'", netName())); 178  } 179  } 180  181  return constants; 182  } 183  184  public boolean isPeerDiscoveryEnabled() { 185  return configFromFiles.getBoolean("peer.discovery.enabled"); 186  } 187  188  public int peerConnectionTimeout() { 189  return configFromFiles.getInt("peer.connection.timeout") * 1000; 190  } 191  192  public int defaultP2PVersion() { 193  return configFromFiles.hasPath("peer.p2p.version") ? configFromFiles.getInt("peer.p2p.version") : P2pHandler.VERSION; 194  } 195  196  public int rlpxMaxFrameSize() { 197  return configFromFiles.hasPath("peer.p2p.framing.maxSize") ? configFromFiles.getInt("peer.p2p.framing.maxSize") : MessageCodec.NO_FRAMING; 198  } 199  200  public List<String> peerDiscoveryIPList() { 201  return configFromFiles.hasPath("peer.discovery.ip.list") ? configFromFiles.getStringList("peer.discovery.ip.list") : new ArrayList<>(); 202  } 203  204  public boolean databaseReset() { 205  return configFromFiles.getBoolean("database.reset"); 206  } 207  208  public boolean importEnabled() { 209  return configFromFiles.getBoolean(PROPERTY_DB_IMPORT); 210  } 211  212  public String importUrl() { 213  return configFromFiles.getString("database.import.url"); 214  } 215  216  public List<String> importTrustedKeys() { 217  return configFromFiles.getStringList("database.import.trusted-keys"); 218  } 219  220  public List<Node> peerActive() { 221  if (!configFromFiles.hasPath("peer.active")) { 222  return Collections.emptyList(); 223  } 224  List<? extends ConfigObject> list = configFromFiles.getObjectList("peer.active"); 225  return list.stream().map(this::parsePeer).collect(Collectors.toList()); 226  } 227  228  private Node parsePeer(ConfigObject configObject) { 229  if (configObject.get("url") != null) { 230  String url = configObject.toConfig().getString("url"); 231  return new Node(url.startsWith("enode://") ? url : "enode://" + url); 232  } 233  234  if (configObject.get("ip") != null) { 235  String ip = configObject.toConfig().getString("ip"); 236  int port = configObject.toConfig().getInt("port"); 237  if (configObject.toConfig().hasPath("nodeId")) { 238  byte[] nodeId = Hex.decode(configObject.toConfig().getString("nodeId").trim()); 239  if (nodeId.length == 64) { 240  return new Node(nodeId, ip, port); 241  } 242  243  throw new RuntimeException("Invalid config nodeId '" + nodeId + "' at " + configObject); 244  } 245  246  if (configObject.toConfig().hasPath("nodeName")) { 247  String nodeName = configObject.toConfig().getString("nodeName").trim(); 248  // FIXME should be sha3-512 here ? 249  byte[] nodeId = ECKey.fromPrivate(Keccak256Helper.keccak256(nodeName.getBytes(StandardCharsets.UTF_8))).getNodeId(); 250  return new Node(nodeId, ip, port); 251  } 252  253  throw new RuntimeException("Either nodeId or nodeName should be specified: " + configObject); 254  } 255  256  throw new RuntimeException("Unexpected element within 'peer.active' config list: " + configObject); 257  } 258  259  public NodeFilter trustedPeers() { 260  List<? extends ConfigObject> list = configFromFiles.getObjectList("peer.trusted"); 261  NodeFilter ret = new NodeFilter(); 262  list.stream().map(ConfigObject::toConfig).forEach(config -> { 263  String nodeIdData = config.getString("nodeId"); 264  String ipData = config.getString("ip"); 265  byte[] nodeId = nodeIdData != null ? Hex.decode(nodeIdData.trim()) : null; 266  String ipMask = ipData != null ? ipData.trim() : null; 267  ret.add(nodeId, ipMask); 268  }); 269  270  return ret; 271  } 272  273  public Integer peerChannelReadTimeout() { 274  return configFromFiles.getInt("peer.channel.read.timeout"); 275  } 276  277  public String dumpStyle() { 278  return configFromFiles.getString("dump.style"); 279  } 280  281  public int dumpBlock() { 282  return configFromFiles.getInt("dump.block"); 283  } 284  285  public String databaseDir() { 286  return databaseDir == null ? configFromFiles.getString(PROPERTY_BASE_PATH) : databaseDir; 287  } 288  289  public void setDataBaseDir(String dataBaseDir) { 290  this.databaseDir = dataBaseDir; 291  } 292  293  public boolean playVM() { 294  return configFromFiles.getBoolean("play.vm"); 295  } 296  297  public int maxHashesAsk() { 298  return configFromFiles.getInt("sync.max.hashes.ask"); 299  } 300  301  public int syncPeerCount() { 302  return configFromFiles.getInt("sync.peer.count"); 303  } 304  305  public Integer syncVersion() { 306  if (!configFromFiles.hasPath("sync.version")) { 307  return null; 308  } 309  return configFromFiles.getInt("sync.version"); 310  } 311  312  313  public String projectVersion() { 314  return projectVersion; 315  } 316  317  public String projectVersionModifier() { 318  return projectVersionModifier; 319  } 320  321  public String helloPhrase() { 322  return configFromFiles.getString("hello.phrase"); 323  } 324  325  public List<String> peerCapabilities() { 326  return configFromFiles.hasPath("peer.capabilities") ? configFromFiles.getStringList("peer.capabilities") : new ArrayList<>(Arrays.asList("rsk")); 327  } 328  329  public boolean vmTrace() { 330  return configFromFiles.getBoolean("vm.structured.trace"); 331  } 332  333  public int vmTraceOptions() { 334  return configFromFiles.getInt("vm.structured.traceOptions"); 335  } 336  337  public boolean vmTraceCompressed() { 338  return configFromFiles.getBoolean("vm.structured.compressed"); 339  } 340  341  public int vmTraceInitStorageLimit() { 342  return configFromFiles.getInt("vm.structured.initStorageLimit"); 343  } 344  345  public String vmTraceDir() { 346  return configFromFiles.getString("vm.structured.dir"); 347  } 348  349  public String privateKey() { 350  if (configFromFiles.hasPath("peer.privateKey")) { 351  String key = configFromFiles.getString("peer.privateKey"); 352  if (key.length() != 64) { 353  throw new RuntimeException("The peer.privateKey needs to be Hex encoded and 32 byte length"); 354  } 355  return key; 356  } else { 357  return getGeneratedNodePrivateKey(); 358  } 359  } 360  361  private String getGeneratedNodePrivateKey() { 362  try { 363  File file = new File(databaseDir(), "nodeId.properties"); 364  Properties props = new Properties(); 365  if (file.canRead()) { 366  try (FileReader reader = new FileReader(file)) { 367  props.load(reader); 368  } 369  } else { 370  ECKey key = new ECKey(); 371  props.setProperty("nodeIdPrivateKey", ByteUtil.toHexString(key.getPrivKeyBytes())); 372  props.setProperty("nodeId", ByteUtil.toHexString(key.getNodeId())); 373  file.getParentFile().mkdirs(); 374  try (FileWriter writer = new FileWriter(file)) { 375  props.store(writer, "Generated NodeID. To use your own nodeId please refer to 'peer.privateKey' config option."); 376  logger.info("New nodeID generated: {}", props.getProperty("nodeId")); 377  logger.info("Generated nodeID and its private key stored in {}", file); 378  } 379  } 380  return props.getProperty("nodeIdPrivateKey"); 381  } catch (IOException e) { 382  throw new RuntimeException(e); 383  } 384  } 385  386  public ECKey getMyKey() { 387  return ECKey.fromPrivate(Hex.decode(privateKey())).decompress(); 388  } 389  390  /** 391  * Home NodeID calculated from 'peer.privateKey' property 392  */ 393  public byte[] nodeId() { 394  return getMyKey().getNodeId(); 395  } 396  397  public int networkId() { 398  return configFromFiles.getInt("peer.networkId"); 399  } 400  401  public int maxActivePeers() { 402  return configFromFiles.getInt("peer.maxActivePeers"); 403  } 404  405  public int maxConnectionsAllowed() { 406  return configFromFiles.getInt("peer.filter.maxConnections"); 407  } 408  409  public int networkCIDR() { 410  return configFromFiles.getInt("peer.filter.networkCidr"); 411  } 412  413  public boolean eip8() { 414  return configFromFiles.getBoolean("peer.p2p.eip8"); 415  } 416  417  public int getPeerPort() { 418  return configFromFiles.getInt(PROPERTY_PEER_PORT); 419  } 420  421  public InetAddress getBindAddress() { 422  String host = configFromFiles.getString(PROPERTY_BIND_ADDRESS); 423  try { 424  return InetAddress.getByName(host); 425  } catch (UnknownHostException e) { 426  throw new IllegalArgumentException(String.format("%s is not a valid %s property", host, PROPERTY_BIND_ADDRESS), e); 427  } 428  } 429  430  /** 431  * This can be a blocking call with long timeout (thus no ValidateMe) 432  */ 433  public synchronized String getPublicIp() { 434  if (publicIp != null) { 435  return publicIp; 436  } 437  438  if (configFromFiles.hasPath(PROPERTY_PUBLIC_IP)) { 439  String externalIpFromConfig = configFromFiles.getString(PROPERTY_PUBLIC_IP).trim(); 440  if (!externalIpFromConfig.isEmpty()) { 441  try { 442  InetAddress address = tryParseIpOrThrow(externalIpFromConfig); 443  publicIp = address.getHostAddress(); 444  logger.info("Public IP identified {}", publicIp); 445  return publicIp; 446  } catch (IllegalArgumentException e) { 447  logger.warn("Can't resolve public IP", e); 448  } 449  publicIp = null; 450  } 451  } 452  453  publicIp = getMyPublicIpFromRemoteService().getHostAddress(); 454  return publicIp; 455  } 456  457  private InetAddress getMyPublicIpFromRemoteService() { 458  try { 459  URL ipCheckService = publicIpCheckService(); 460  logger.info("Public IP wasn't set or resolved, using {} to identify it...", ipCheckService); 461  462  String ipFromService; 463  try (BufferedReader in = new BufferedReader(new InputStreamReader(ipCheckService.openStream()))) { 464  ipFromService = in.readLine(); 465  } 466  467  if (ipFromService == null || ipFromService.trim().isEmpty()) { 468  logger.warn("Unable to retrieve public IP from {} {}.", ipCheckService, ipFromService); 469  throw new IOException("Invalid address: '" + ipFromService + "'"); 470  } 471  472  InetAddress resolvedIp = tryParseIpOrThrow(ipFromService); 473  logger.info("Identified public IP: {}", resolvedIp); 474  return resolvedIp; 475  } catch (IOException e) { 476  logger.error("Can't get public IP", e); 477  } catch (IllegalArgumentException e) { 478  logger.error("Can't get public IP", e); 479  } 480  481  InetAddress bindAddress = getBindAddress(); 482  if (bindAddress.isAnyLocalAddress()) { 483  throw new RuntimeException("Wildcard on bind address it's not allowed as fallback for public IP " + bindAddress); 484  } 485  486  return bindAddress; 487  } 488  489  private URL publicIpCheckService() throws MalformedURLException { 490  return new URL(configFromFiles.getString("public.ipCheckService")); 491  } 492  493  public boolean isSyncEnabled() { 494  return configFromFiles.getBoolean("sync.enabled"); 495  } 496  497  public String genesisInfo() { 498  499  if (genesisInfo == null) { 500  return configFromFiles.getString("genesis"); 501  } else { 502  return genesisInfo; 503  } 504  } 505  506  public int txOutdatedThreshold() { 507  return configFromFiles.getInt("transaction.outdated.threshold"); 508  } 509  510  public int txOutdatedTimeout() { 511  return configFromFiles.getInt("transaction.outdated.timeout"); 512  } 513  514  public void setGenesisInfo(String genesisInfo) { 515  this.genesisInfo = genesisInfo; 516  } 517  518  public boolean scoringPunishmentEnabled() { 519  return configFromFiles.hasPath("scoring.punishmentEnabled") ? 520  configFromFiles.getBoolean("scoring.punishmentEnabled") : false; 521  } 522  523  public int scoringNumberOfNodes() { 524  return getInt("scoring.nodes.number", 100); 525  } 526  527  public long scoringNodesPunishmentDuration() { 528  return getLong("scoring.nodes.duration", 10) * 60000L; 529  } 530  531  public int scoringNodesPunishmentIncrement() { 532  return getInt("scoring.nodes.increment", 10); 533  } 534  535  public long scoringNodesPunishmentMaximumDuration() { 536  // default value: no maximum duration 537  return getLong("scoring.nodes.maximum", 0) * 60000L; 538  } 539  540  public long scoringAddressesPunishmentDuration() { 541  return getLong("scoring.addresses.duration", 10) * 60000L; 542  } 543  544  public int scoringAddressesPunishmentIncrement() { 545  return getInt("scoring.addresses.increment", 10); 546  } 547  548  public long scoringAddressesPunishmentMaximumDuration() { 549  // default value: 1 week 550  return TimeUnit.MINUTES.toMillis(getLong("scoring.addresses.maximum", TimeUnit.DAYS.toMinutes(7))); 551  } 552  553  public boolean shouldPrintSystemInfo() { 554  return getBoolean(PROPERTY_PRINT_SYSTEM_INFO, false); 555  } 556  557  public boolean shouldSkipJavaVersionCheck() { 558  return getBoolean(PROPERTY_SKIP_JAVA_VERSION_CHECK, false); 559  } 560  561  protected int getInt(String path, int val) { 562  return configFromFiles.hasPath(path) ? configFromFiles.getInt(path) : val; 563  } 564  565  protected long getLong(String path, long val) { 566  return configFromFiles.hasPath(path) ? configFromFiles.getLong(path) : val; 567  } 568  569  protected double getDouble(String path, double val) { 570  return configFromFiles.hasPath(path) ? configFromFiles.getDouble(path) : val; 571  } 572  573  protected boolean getBoolean(String path, boolean val) { 574  return configFromFiles.hasPath(path) ? configFromFiles.getBoolean(path) : val; 575  } 576  577  protected String getString(String path, String val) { 578  return configFromFiles.hasPath(path) ? configFromFiles.getString(path) : val; 579  } 580  581  /* 582  * 583  * Testing 584  * 585  */ 586  public boolean vmTestLoadLocal() { 587  return configFromFiles.hasPath("GitHubTests.VMTest.loadLocal") ? 588  configFromFiles.getBoolean("GitHubTests.VMTest.loadLocal") : DEFAULT_VMTEST_LOAD_LOCAL; 589  } 590  591  public String customSolcPath() { 592  return configFromFiles.hasPath("solc.path") ? configFromFiles.getString("solc.path") : null; 593  } 594  595  public String netName() { 596  return configFromFiles.getString(PROPERTY_BC_CONFIG_NAME); 597  } 598  599  public boolean isRpcHttpEnabled() { 600  return configFromFiles.getBoolean(PROPERTY_RPC_HTTP_ENABLED); 601  } 602  603  public boolean isRpcWebSocketEnabled() { 604  return configFromFiles.getBoolean(PROPERTY_RPC_WEBSOCKET_ENABLED); 605  } 606  607  public int rpcHttpPort() { 608  return configFromFiles.getInt(PROPERTY_RPC_HTTP_PORT); 609  } 610  611  public int rpcWebSocketPort() { 612  return configFromFiles.getInt(PROPERTY_RPC_WEBSOCKET_PORT); 613  } 614  615  public InetAddress rpcHttpBindAddress() { 616  return getWebBindAddress(PROPERTY_RPC_HTTP_ADDRESS); 617  } 618  619  public InetAddress rpcWebSocketBindAddress() { 620  return getWebBindAddress(PROPERTY_RPC_WEBSOCKET_ADDRESS); 621  } 622  623  public List<String> rpcHttpHost() { 624  return configFromFiles.getStringList(PROPERTY_RPC_HTTP_HOSTS); 625  } 626  627  private InetAddress getWebBindAddress(String bindAddressConfigKey) { 628  String bindAddress = configFromFiles.getString(bindAddressConfigKey); 629  try { 630  return InetAddress.getByName(bindAddress); 631  } catch (UnknownHostException e) { 632  logger.warn("Unable to bind to {}. Using loopback instead", e); 633  return InetAddress.getLoopbackAddress(); 634  } 635  } 636  637  public String corsDomains() { 638  return configFromFiles.getString(PROPERTY_RPC_CORS); 639  } 640  641  /** 642  * Parses a list of IPs separated by commas. E.g. "171.99.160.48, 171.99.160.48". 643  */ 644  private InetAddress tryParseIpOrThrow(String ipsToParse) { 645  try { 646  String[] ips = ipsToParse.split(", "); 647  String ipToParse = ips[ips.length - 1]; 648  return InetAddress.getByName(ipToParse); 649  } catch (UnknownHostException e) { 650  throw new IllegalArgumentException("Invalid address(es): '" + ipsToParse + "'", e); 651  } 652  } 653  654  private Optional<List<BtcECKey>> getGenesisFederationPublicKeys() { 655  if (!configFromFiles.hasPath(PROPERTY_GENESIS_CONSTANTS_FEDERATION_PUBLICKEYS)) { 656  return Optional.empty(); 657  } 658  659  List<String> configFederationPublicKeys = configFromFiles.getStringList(PROPERTY_GENESIS_CONSTANTS_FEDERATION_PUBLICKEYS); 660  return Optional.of( 661  configFederationPublicKeys.stream() 662  .map(key -> BtcECKey.fromPublicOnly(Hex.decode(key))).collect(Collectors.toList()) 663  ); 664  } 665 }