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 }