[PIE-1809] Clean up genesis parsing (#1809)

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
mbaxter 5 years ago committed by GitHub
parent 75b754e36b
commit 63ffdb5f02
  1. 1
      config/build.gradle
  2. 13
      config/src/main/java/tech/pegasys/pantheon/config/CliqueConfigOptions.java
  3. 45
      config/src/main/java/tech/pegasys/pantheon/config/ConfigUtil.java
  4. 12
      config/src/main/java/tech/pegasys/pantheon/config/EthashConfigOptions.java
  5. 27
      config/src/main/java/tech/pegasys/pantheon/config/GenesisAllocation.java
  6. 81
      config/src/main/java/tech/pegasys/pantheon/config/GenesisConfigFile.java
  7. 44
      config/src/main/java/tech/pegasys/pantheon/config/IbftConfigOptions.java
  8. 60
      config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java
  9. 243
      config/src/main/java/tech/pegasys/pantheon/config/JsonUtil.java
  10. 12
      config/src/test/java/tech/pegasys/pantheon/config/CliqueConfigOptionsTest.java
  11. 39
      config/src/test/java/tech/pegasys/pantheon/config/GenesisConfigFileTest.java
  12. 11
      config/src/test/java/tech/pegasys/pantheon/config/GenesisConfigOptionsTest.java
  13. 12
      config/src/test/java/tech/pegasys/pantheon/config/IbftConfigOptionsTest.java
  14. 545
      config/src/test/java/tech/pegasys/pantheon/config/JsonUtilTest.java
  15. 3
      consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueMinerExecutorTest.java
  16. 21
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/chain/GenesisState.java
  17. 14
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetProtocolScheduleTest.java
  18. 6
      ethereum/permissioning/src/test/java/tech/pegasys/pantheon/ethereum/permissioning/NodeSmartContractPermissioningControllerTest.java
  19. 56
      pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/operator/OperatorSubCommand.java
  20. 40
      pantheon/src/test/java/tech/pegasys/pantheon/cli/operator/OperatorSubCommandTest.java
  21. 44
      pantheon/src/test/resources/operator/config_import_keys_invalid_keys.json

@ -28,7 +28,6 @@ jar {
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.google.guava:guava'
implementation 'io.vertx:vertx-core'
implementation 'org.apache.logging.log4j:log4j-api'
runtime 'org.apache.logging.log4j:log4j-core'

@ -14,28 +14,29 @@ package tech.pegasys.pantheon.config;
import java.util.Map;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.json.JsonObject;
public class CliqueConfigOptions {
public static final CliqueConfigOptions DEFAULT = new CliqueConfigOptions(new JsonObject());
public static final CliqueConfigOptions DEFAULT =
new CliqueConfigOptions(JsonUtil.createEmptyObjectNode());
private static final long DEFAULT_EPOCH_LENGTH = 30_000;
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 15;
private final JsonObject cliqueConfigRoot;
private final ObjectNode cliqueConfigRoot;
CliqueConfigOptions(final JsonObject cliqueConfigRoot) {
CliqueConfigOptions(final ObjectNode cliqueConfigRoot) {
this.cliqueConfigRoot = cliqueConfigRoot;
}
public long getEpochLength() {
return cliqueConfigRoot.getLong("epochlength", DEFAULT_EPOCH_LENGTH);
return JsonUtil.getLong(cliqueConfigRoot, "epochlength", DEFAULT_EPOCH_LENGTH);
}
public int getBlockPeriodSeconds() {
return cliqueConfigRoot.getInteger("blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
return JsonUtil.getInt(cliqueConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
}
Map<String, Object> asMap() {

@ -1,45 +0,0 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.config;
import java.math.BigInteger;
import java.util.Optional;
import java.util.OptionalLong;
import io.vertx.core.json.JsonObject;
public class ConfigUtil {
public static OptionalLong getOptionalLong(final JsonObject jsonObject, final String key) {
return jsonObject.containsKey(key)
? OptionalLong.of(jsonObject.getLong(key))
: OptionalLong.empty();
}
public static Optional<BigInteger> getOptionalBigInteger(
final JsonObject jsonObject, final String key) {
return jsonObject.containsKey(key)
? Optional.ofNullable(getBigInteger(jsonObject, key))
: Optional.empty();
}
private static BigInteger getBigInteger(final JsonObject jsonObject, final String key) {
final Number value = (Number) jsonObject.getMap().get(key);
if (value == null) {
return null;
} else if (value instanceof BigInteger) {
return (BigInteger) value;
} else {
return BigInteger.valueOf(value.longValue());
}
}
}

@ -15,20 +15,22 @@ package tech.pegasys.pantheon.config;
import java.util.Map;
import java.util.OptionalLong;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.json.JsonObject;
public class EthashConfigOptions {
public static final EthashConfigOptions DEFAULT = new EthashConfigOptions(new JsonObject());;
private final JsonObject ethashConfigRoot;
public static final EthashConfigOptions DEFAULT =
new EthashConfigOptions(JsonUtil.createEmptyObjectNode());
EthashConfigOptions(final JsonObject ethashConfigRoot) {
private final ObjectNode ethashConfigRoot;
EthashConfigOptions(final ObjectNode ethashConfigRoot) {
this.ethashConfigRoot = ethashConfigRoot;
}
public OptionalLong getFixedDifficulty() {
return ConfigUtil.getOptionalLong(ethashConfigRoot, "fixeddifficulty");
return JsonUtil.getLong(ethashConfigRoot, "fixeddifficulty");
}
Map<String, Object> asMap() {

@ -12,15 +12,16 @@
*/
package tech.pegasys.pantheon.config;
import java.util.HashMap;
import java.util.Map;
import io.vertx.core.json.JsonObject;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class GenesisAllocation {
private final String address;
private final JsonObject data;
private final ObjectNode data;
GenesisAllocation(final String address, final JsonObject data) {
GenesisAllocation(final String address, final ObjectNode data) {
this.address = address;
this.data = data;
}
@ -30,22 +31,30 @@ public class GenesisAllocation {
}
public String getBalance() {
return data.getString("balance", "0");
return JsonUtil.getValueAsString(data, "balance", "0");
}
public String getCode() {
return data.getString("code");
return JsonUtil.getString(data, "code", null);
}
public String getNonce() {
return data.getString("nonce", "0");
return JsonUtil.getValueAsString(data, "nonce", "0");
}
public String getVersion() {
return data.getString("version");
return JsonUtil.getValueAsString(data, "version", null);
}
public Map<String, Object> getStorage() {
return data.getJsonObject("storage", new JsonObject()).getMap();
public Map<String, String> getStorage() {
final Map<String, String> map = new HashMap<>();
JsonUtil.getObjectNode(data, "storage")
.orElse(JsonUtil.createEmptyObjectNode())
.fields()
.forEachRemaining(
(entry) -> {
map.put(entry.getKey(), entry.getValue().asText());
});
return map;
}
}

@ -15,22 +15,23 @@ package tech.pegasys.pantheon.config;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Streams;
import com.google.common.io.Resources;
import io.vertx.core.json.JsonObject;
public class GenesisConfigFile {
public static final GenesisConfigFile DEFAULT = new GenesisConfigFile(new JsonObject());
public static final GenesisConfigFile DEFAULT =
new GenesisConfigFile(JsonUtil.createEmptyObjectNode());
private final JsonObject configRoot;
private final ObjectNode configRoot;
private GenesisConfigFile(final JsonObject config) {
private GenesisConfigFile(final ObjectNode config) {
this.configRoot = config;
}
@ -52,26 +53,36 @@ public class GenesisConfigFile {
}
}
public static GenesisConfigFile fromConfig(final String config) {
return fromConfig(new JsonObject(config));
public static GenesisConfigFile fromConfig(final String jsonString) {
// TODO: Should we disable comments?
final boolean allowComments = true;
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonString, allowComments);
return fromConfig(rootNode);
}
public static GenesisConfigFile fromConfig(final JsonObject config) {
public static GenesisConfigFile fromConfig(final ObjectNode config) {
return new GenesisConfigFile(normalizeKeys(config));
}
public GenesisConfigOptions getConfigOptions() {
return new JsonGenesisConfigOptions(configRoot.getJsonObject("config"));
ObjectNode config =
JsonUtil.getObjectNode(configRoot, "config").orElse(JsonUtil.createEmptyObjectNode());
return new JsonGenesisConfigOptions(config);
}
public Stream<GenesisAllocation> streamAllocations() {
final JsonObject allocations = configRoot.getJsonObject("alloc", new JsonObject());
return allocations.fieldNames().stream()
.map(key -> new GenesisAllocation(key, allocations.getJsonObject(key)));
return JsonUtil.getObjectNode(configRoot, "alloc").stream()
.flatMap(
allocations ->
Streams.stream(allocations.fieldNames())
.map(
key ->
new GenesisAllocation(
key, JsonUtil.getObjectNode(allocations, key).get())));
}
public String getParentHash() {
return configRoot.getString("parenthash", "");
return JsonUtil.getString(configRoot, "parenthash", "");
}
public String getDifficulty() {
@ -79,7 +90,7 @@ public class GenesisConfigFile {
}
public String getExtraData() {
return configRoot.getString("extradata", "");
return JsonUtil.getString(configRoot, "extradata", "");
}
public long getGasLimit() {
@ -87,27 +98,27 @@ public class GenesisConfigFile {
}
public String getMixHash() {
return configRoot.getString("mixhash", "");
return JsonUtil.getString(configRoot, "mixhash", "");
}
public String getNonce() {
return configRoot.getString("nonce", "0x0");
return JsonUtil.getValueAsString(configRoot, "nonce", "0x0");
}
public Optional<String> getCoinbase() {
return Optional.ofNullable(configRoot.getString("coinbase"));
return JsonUtil.getString(configRoot, "coinbase");
}
public long getTimestamp() {
return parseLong("timestamp", configRoot.getString("timestamp", "0x0"));
return parseLong("timestamp", JsonUtil.getValueAsString(configRoot, "timestamp", "0x0"));
}
private String getRequiredString(final String key) {
if (!configRoot.containsKey(key)) {
if (!configRoot.has(key)) {
throw new IllegalArgumentException(
String.format("Invalid genesis block configuration, missing value for '%s'", key));
}
return configRoot.getString(key);
return configRoot.get(key).asText();
}
private long parseLong(final String name, final String value) {
@ -123,28 +134,24 @@ public class GenesisConfigFile {
}
}
/* Converts the {@link JsonObject} describing the Genesis Block to a {@link Map}. This method
* converts all nested {@link JsonObject} to {@link Map} as well. Also, note that all keys are
* converted to lowercase for easier lookup since the keys in a 'genesis.json' file are assumed
/* Converts all to lowercase for easier lookup since the keys in a 'genesis.json' file are assumed
* case insensitive.
*/
@SuppressWarnings("unchecked")
private static JsonObject normalizeKeys(final JsonObject genesis) {
final Map<String, Object> normalized = new HashMap<>();
private static ObjectNode normalizeKeys(final ObjectNode genesis) {
final ObjectNode normalized = JsonUtil.createEmptyObjectNode();
genesis
.getMap()
.forEach(
(key, value) -> {
.fields()
.forEachRemaining(
entry -> {
final String key = entry.getKey();
final JsonNode value = entry.getValue();
final String normalizedKey = key.toLowerCase(Locale.US);
if (value instanceof JsonObject) {
normalized.put(normalizedKey, normalizeKeys((JsonObject) value));
} else if (value instanceof Map) {
normalized.put(
normalizedKey, normalizeKeys(new JsonObject((Map<String, Object>) value)));
if (value instanceof ObjectNode) {
normalized.set(normalizedKey, normalizeKeys((ObjectNode) value));
} else {
normalized.put(normalizedKey, value);
normalized.set(normalizedKey, value);
}
});
return new JsonObject(normalized);
return normalized;
}
}

@ -14,12 +14,13 @@ package tech.pegasys.pantheon.config;
import java.util.Map;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.json.JsonObject;
public class IbftConfigOptions {
public static final IbftConfigOptions DEFAULT = new IbftConfigOptions(new JsonObject());
public static final IbftConfigOptions DEFAULT =
new IbftConfigOptions(JsonUtil.createEmptyObjectNode());
private static final long DEFAULT_EPOCH_LENGTH = 30_000;
private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1;
@ -32,69 +33,70 @@ public class IbftConfigOptions {
private static final int DEFAULT_FUTURE_MESSAGES_LIMIT = 1000;
private static final int DEFAULT_FUTURE_MESSAGES_MAX_DISTANCE = 10;
private final JsonObject ibftConfigRoot;
private final ObjectNode ibftConfigRoot;
IbftConfigOptions(final JsonObject ibftConfigRoot) {
IbftConfigOptions(final ObjectNode ibftConfigRoot) {
this.ibftConfigRoot = ibftConfigRoot;
}
public long getEpochLength() {
return ibftConfigRoot.getLong("epochlength", DEFAULT_EPOCH_LENGTH);
return JsonUtil.getLong(ibftConfigRoot, "epochlength", DEFAULT_EPOCH_LENGTH);
}
public int getBlockPeriodSeconds() {
return ibftConfigRoot.getInteger("blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
return JsonUtil.getInt(ibftConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS);
}
public int getRequestTimeoutSeconds() {
return ibftConfigRoot.getInteger("requesttimeoutseconds", DEFAULT_ROUND_EXPIRY_SECONDS);
return JsonUtil.getInt(ibftConfigRoot, "requesttimeoutseconds", DEFAULT_ROUND_EXPIRY_SECONDS);
}
public int getGossipedHistoryLimit() {
return ibftConfigRoot.getInteger("gossipedhistorylimit", DEFAULT_GOSSIPED_HISTORY_LIMIT);
return JsonUtil.getInt(ibftConfigRoot, "gossipedhistorylimit", DEFAULT_GOSSIPED_HISTORY_LIMIT);
}
public int getMessageQueueLimit() {
return ibftConfigRoot.getInteger("messagequeuelimit", DEFAULT_MESSAGE_QUEUE_LIMIT);
return JsonUtil.getInt(ibftConfigRoot, "messagequeuelimit", DEFAULT_MESSAGE_QUEUE_LIMIT);
}
public int getDuplicateMessageLimit() {
return ibftConfigRoot.getInteger("duplicatemessagelimit", DEFAULT_DUPLICATE_MESSAGE_LIMIT);
return JsonUtil.getInt(
ibftConfigRoot, "duplicatemessagelimit", DEFAULT_DUPLICATE_MESSAGE_LIMIT);
}
public int getFutureMessagesLimit() {
return ibftConfigRoot.getInteger("futuremessageslimit", DEFAULT_FUTURE_MESSAGES_LIMIT);
return JsonUtil.getInt(ibftConfigRoot, "futuremessageslimit", DEFAULT_FUTURE_MESSAGES_LIMIT);
}
public int getFutureMessagesMaxDistance() {
return ibftConfigRoot.getInteger(
"futuremessagesmaxdistance", DEFAULT_FUTURE_MESSAGES_MAX_DISTANCE);
return JsonUtil.getInt(
ibftConfigRoot, "futuremessagesmaxdistance", DEFAULT_FUTURE_MESSAGES_MAX_DISTANCE);
}
Map<String, Object> asMap() {
final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
if (ibftConfigRoot.containsKey("epochlength")) {
if (ibftConfigRoot.has("epochlength")) {
builder.put("epochLength", getEpochLength());
}
if (ibftConfigRoot.containsKey("blockperiodseconds")) {
if (ibftConfigRoot.has("blockperiodseconds")) {
builder.put("blockPeriodSeconds", getBlockPeriodSeconds());
}
if (ibftConfigRoot.containsKey("requesttimeoutseconds")) {
if (ibftConfigRoot.has("requesttimeoutseconds")) {
builder.put("requestTimeoutSeconds", getRequestTimeoutSeconds());
}
if (ibftConfigRoot.containsKey("gossipedhistorylimit")) {
if (ibftConfigRoot.has("gossipedhistorylimit")) {
builder.put("gossipedHistoryLimit", getGossipedHistoryLimit());
}
if (ibftConfigRoot.containsKey("messagequeuelimit")) {
if (ibftConfigRoot.has("messagequeuelimit")) {
builder.put("messageQueueLimit", getMessageQueueLimit());
}
if (ibftConfigRoot.containsKey("duplicatemessagelimit")) {
if (ibftConfigRoot.has("duplicatemessagelimit")) {
builder.put("duplicateMessageLimit", getDuplicateMessageLimit());
}
if (ibftConfigRoot.containsKey("futuremessageslimit")) {
if (ibftConfigRoot.has("futuremessageslimit")) {
builder.put("futureMessagesLimit", getFutureMessagesLimit());
}
if (ibftConfigRoot.containsKey("futuremessagesmaxdistance")) {
if (ibftConfigRoot.has("futuremessagesmaxdistance")) {
builder.put("futureMessagesMaxDistance", getFutureMessagesMaxDistance());
}
return builder.build();

@ -12,7 +12,7 @@
*/
package tech.pegasys.pantheon.config;
import static tech.pegasys.pantheon.config.ConfigUtil.getOptionalBigInteger;
import static java.util.Objects.isNull;
import java.math.BigInteger;
import java.util.Map;
@ -20,8 +20,8 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import io.vertx.core.json.JsonObject;
public class JsonGenesisConfigOptions implements GenesisConfigOptions {
@ -29,62 +29,62 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
private static final String IBFT_LEGACY_CONFIG_KEY = "ibft";
private static final String IBFT2_CONFIG_KEY = "ibft2";
private static final String CLIQUE_CONFIG_KEY = "clique";
private final JsonObject configRoot;
private final ObjectNode configRoot;
public static JsonGenesisConfigOptions fromJsonObject(final JsonObject configRoot) {
public static JsonGenesisConfigOptions fromJsonObject(final ObjectNode configRoot) {
return new JsonGenesisConfigOptions(configRoot);
}
JsonGenesisConfigOptions(final JsonObject configRoot) {
this.configRoot = configRoot != null ? configRoot : new JsonObject();
JsonGenesisConfigOptions(final ObjectNode maybeConfig) {
this.configRoot = isNull(maybeConfig) ? JsonUtil.createEmptyObjectNode() : maybeConfig;
}
@Override
public boolean isEthHash() {
return configRoot.containsKey(ETHASH_CONFIG_KEY);
return configRoot.has(ETHASH_CONFIG_KEY);
}
@Override
public boolean isIbftLegacy() {
return configRoot.containsKey(IBFT_LEGACY_CONFIG_KEY);
return configRoot.has(IBFT_LEGACY_CONFIG_KEY);
}
@Override
public boolean isClique() {
return configRoot.containsKey(CLIQUE_CONFIG_KEY);
return configRoot.has(CLIQUE_CONFIG_KEY);
}
@Override
public boolean isIbft2() {
return configRoot.containsKey(IBFT2_CONFIG_KEY);
return configRoot.has(IBFT2_CONFIG_KEY);
}
@Override
public IbftConfigOptions getIbftLegacyConfigOptions() {
return isIbftLegacy()
? new IbftConfigOptions(configRoot.getJsonObject(IBFT_LEGACY_CONFIG_KEY))
: IbftConfigOptions.DEFAULT;
return JsonUtil.getObjectNode(configRoot, IBFT_LEGACY_CONFIG_KEY)
.map(IbftConfigOptions::new)
.orElse(IbftConfigOptions.DEFAULT);
}
@Override
public IbftConfigOptions getIbft2ConfigOptions() {
return isIbft2()
? new IbftConfigOptions(configRoot.getJsonObject(IBFT2_CONFIG_KEY))
: IbftConfigOptions.DEFAULT;
return JsonUtil.getObjectNode(configRoot, IBFT2_CONFIG_KEY)
.map(IbftConfigOptions::new)
.orElse(IbftConfigOptions.DEFAULT);
}
@Override
public CliqueConfigOptions getCliqueConfigOptions() {
return isClique()
? new CliqueConfigOptions(configRoot.getJsonObject(CLIQUE_CONFIG_KEY))
: CliqueConfigOptions.DEFAULT;
return JsonUtil.getObjectNode(configRoot, CLIQUE_CONFIG_KEY)
.map(CliqueConfigOptions::new)
.orElse(CliqueConfigOptions.DEFAULT);
}
@Override
public EthashConfigOptions getEthashConfigOptions() {
return isEthHash()
? new EthashConfigOptions(configRoot.getJsonObject(ETHASH_CONFIG_KEY))
: EthashConfigOptions.DEFAULT;
return JsonUtil.getObjectNode(configRoot, ETHASH_CONFIG_KEY)
.map(EthashConfigOptions::new)
.orElse(EthashConfigOptions.DEFAULT);
}
@Override
@ -133,21 +133,17 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
@Override
public Optional<BigInteger> getChainId() {
return getOptionalBigInteger(configRoot, "chainid");
return JsonUtil.getValueAsString(configRoot, "chainid").map(BigInteger::new);
}
@Override
public OptionalInt getContractSizeLimit() {
return configRoot.containsKey("contractsizelimit")
? OptionalInt.of(configRoot.getInteger("contractsizelimit"))
: OptionalInt.empty();
return JsonUtil.getInt(configRoot, "contractsizelimit");
}
@Override
public OptionalInt getEvmStackSize() {
return configRoot.containsKey("evmstacksize")
? OptionalInt.of(configRoot.getInteger("evmstacksize"))
: OptionalInt.empty();
return JsonUtil.getInt(configRoot, "evmstacksize");
}
@Override
@ -165,8 +161,8 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
.ifPresent(
l -> {
builder.put("eip150Block", l);
if (configRoot.containsKey("eip150hash")) {
builder.put("eip150Hash", configRoot.getString("eip150hash"));
if (configRoot.has("eip150hash")) {
builder.put("eip150Hash", configRoot.get("eip150hash").asText());
}
});
getSpuriousDragonBlockNumber()
@ -197,6 +193,6 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions {
}
private OptionalLong getOptionalLong(final String key) {
return ConfigUtil.getOptionalLong(configRoot, key);
return JsonUtil.getLong(configRoot, key);
}
}

@ -0,0 +1,243 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.config;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class JsonUtil {
/**
* Get the string representation of the value at {@code key}. For example, a numeric value like 5
* will be returned as "5".
*
* @param node The {@code ObjectNode} from which the value will be extracted.
* @param key The key corresponding to the value to extract.
* @return The value at the given key as a string if it exists.
*/
public static Optional<String> getValueAsString(final ObjectNode node, final String key) {
return getValue(node, key).map(JsonNode::asText);
}
/**
* Get the string representation of the value at {@code key}. For example, a numeric value like 5
* will be returned as "5".
*
* @param node The {@code ObjectNode} from which the value will be extracted.
* @param key The key corresponding to the value to extract.
* @param defaultValue The value to return if no value is found at {@code key}.
* @return The value at the given key as a string if it exists, otherwise {@code defaultValue}
*/
public static String getValueAsString(
final ObjectNode node, final String key, final String defaultValue) {
return getValueAsString(node, key).orElse(defaultValue);
}
/**
* Returns textual (string) value at {@code key}. See {@link #getValueAsString} for retrieving
* non-textual values in string form.
*
* @param node The {@code ObjectNode} from which the value will be extracted.
* @param key The key corresponding to the value to extract.
* @return The textual value at {@code key} if it exists.
*/
public static Optional<String> getString(final ObjectNode node, final String key) {
return getValue(node, key)
.filter(jsonNode -> validateType(jsonNode, JsonNodeType.STRING))
.map(JsonNode::asText);
}
/**
* Returns textual (string) value at {@code key}. See {@link #getValueAsString} for retrieving
* non-textual values in string form.
*
* @param node The {@code ObjectNode} from which the value will be extracted.
* @param key The key corresponding to the value to extract.
* @param defaultValue The value to return if no value is found at {@code key}.
* @return The textual value at {@code key} if it exists, otherwise {@code defaultValue}
*/
public static String getString(
final ObjectNode node, final String key, final String defaultValue) {
return getString(node, key).orElse(defaultValue);
}
public static OptionalInt getInt(final ObjectNode node, final String key) {
return getValue(node, key)
.filter(jsonNode -> validateType(jsonNode, JsonNodeType.NUMBER))
.filter(JsonUtil::validateInt)
.map(JsonNode::asInt)
.map(OptionalInt::of)
.orElse(OptionalInt.empty());
}
public static int getInt(final ObjectNode node, final String key, final int defaultValue) {
return getInt(node, key).orElse(defaultValue);
}
public static OptionalLong getLong(final ObjectNode json, final String key) {
return getValue(json, key)
.filter(jsonNode -> validateType(jsonNode, JsonNodeType.NUMBER))
.filter(JsonUtil::validateLong)
.map(JsonNode::asLong)
.map(OptionalLong::of)
.orElse(OptionalLong.empty());
}
public static long getLong(final ObjectNode json, final String key, final long defaultValue) {
return getLong(json, key).orElse(defaultValue);
}
public static Optional<Boolean> getBoolean(final ObjectNode node, final String key) {
return getValue(node, key)
.filter(jsonNode -> validateType(jsonNode, JsonNodeType.BOOLEAN))
.map(JsonNode::asBoolean);
}
public static boolean getBoolean(
final ObjectNode node, final String key, final boolean defaultValue) {
return getBoolean(node, key).orElse(defaultValue);
}
public static ObjectNode createEmptyObjectNode() {
ObjectMapper mapper = getObjectMapper();
return mapper.createObjectNode();
}
public static ObjectNode objectNodeFromMap(final Map<String, Object> map) {
return (ObjectNode) getObjectMapper().valueToTree(map);
}
public static ObjectNode objectNodeFromString(final String jsonData) {
return objectNodeFromString(jsonData, false);
}
public static ObjectNode objectNodeFromString(
final String jsonData, final boolean allowComments) {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(Feature.ALLOW_COMMENTS, allowComments);
try {
final JsonNode jsonNode = objectMapper.readTree(jsonData);
validateType(jsonNode, JsonNodeType.OBJECT);
return (ObjectNode) jsonNode;
} catch (IOException e) {
// Reading directly from a string should not raise an IOException, just catch and rethrow
throw new RuntimeException(e);
}
}
public static String getJson(final Object objectNode) throws JsonProcessingException {
return getJson(objectNode, true);
}
public static String getJson(final Object objectNode, final boolean prettyPrint)
throws JsonProcessingException {
ObjectMapper mapper = getObjectMapper();
if (prettyPrint) {
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectNode);
} else {
return mapper.writeValueAsString(objectNode);
}
}
public static ObjectMapper getObjectMapper() {
return new ObjectMapper();
}
public static Optional<ObjectNode> getObjectNode(final ObjectNode json, final String fieldKey) {
return getObjectNode(json, fieldKey, true);
}
public static Optional<ObjectNode> getObjectNode(
final ObjectNode json, final String fieldKey, final boolean strict) {
final JsonNode obj = json.get(fieldKey);
if (obj == null || obj.isNull()) {
return Optional.empty();
}
if (!obj.isObject()) {
if (strict) {
validateType(obj, JsonNodeType.OBJECT);
} else {
return Optional.empty();
}
}
return Optional.of((ObjectNode) obj);
}
public static Optional<ArrayNode> getArrayNode(final ObjectNode json, final String fieldKey) {
return getArrayNode(json, fieldKey, true);
}
public static Optional<ArrayNode> getArrayNode(
final ObjectNode json, final String fieldKey, final boolean strict) {
final JsonNode obj = json.get(fieldKey);
if (obj == null || obj.isNull()) {
return Optional.empty();
}
if (!obj.isArray()) {
if (strict) {
validateType(obj, JsonNodeType.ARRAY);
} else {
return Optional.empty();
}
}
return Optional.of((ArrayNode) obj);
}
private static Optional<JsonNode> getValue(final ObjectNode node, final String key) {
JsonNode jsonNode = node.get(key);
if (jsonNode == null || jsonNode.isNull()) {
return Optional.empty();
}
return Optional.of(jsonNode);
}
private static boolean validateType(final JsonNode node, final JsonNodeType expectedType) {
if (node.getNodeType() != expectedType) {
final String errorMessage =
String.format(
"Expected %s value but got %s",
expectedType.toString().toLowerCase(), node.getNodeType().toString().toLowerCase());
throw new IllegalArgumentException(errorMessage);
}
return true;
}
private static boolean validateLong(final JsonNode node) {
if (!node.canConvertToLong()) {
throw new IllegalArgumentException("Cannot convert value to long: " + node.toString());
}
return true;
}
private static boolean validateInt(final JsonNode node) {
if (!node.canConvertToInt()) {
throw new IllegalArgumentException("Cannot convert value to integer: " + node.toString());
}
return true;
}
}

@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.Map;
import io.vertx.core.json.JsonObject;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
public class CliqueConfigOptionsTest {
@ -63,9 +63,11 @@ public class CliqueConfigOptionsTest {
}
private CliqueConfigOptions fromConfigOptions(final Map<String, Object> cliqueConfigOptions) {
return GenesisConfigFile.fromConfig(
new JsonObject(singletonMap("config", singletonMap("clique", cliqueConfigOptions))))
.getConfigOptions()
.getCliqueConfigOptions();
final ObjectNode rootNode = JsonUtil.createEmptyObjectNode();
final ObjectNode configNode = JsonUtil.createEmptyObjectNode();
final ObjectNode options = JsonUtil.objectNodeFromMap(cliqueConfigOptions);
configNode.set("clique", options);
rootNode.set("config", configNode);
return GenesisConfigFile.fromConfig(rootNode).getConfigOptions().getCliqueConfigOptions();
}
}

@ -14,10 +14,10 @@ package tech.pegasys.pantheon.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import java.math.BigInteger;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
@ -148,21 +148,42 @@ public class GenesisConfigFileTest {
+ " \"balance\": \"1000\""
+ " },"
+ " \"f17f52151EbEF6C7334FAD080c5704D77216b732\": {"
+ " \"balance\": \"90000000000000000000000\""
+ " \"balance\": \"90000000000000000000000\","
+ " \"storage\": {"
+ " \"0xc4c3a3f99b26e5e534b71d6f33ca6ea5c174decfb16dd7237c60eff9774ef4a4\": \"0x937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0\",\n"
+ " \"0xc4c3a3f99b26e5e534b71d6f33ca6ea5c174decfb16dd7237c60eff9774ef4a3\": \"0x6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012\""
+ " }"
+ " }"
+ " }"
+ "}");
final Map<String, String> allocations =
final Map<String, GenesisAllocation> allocations =
config
.streamAllocations()
.collect(
Collectors.toMap(GenesisAllocation::getAddress, GenesisAllocation::getBalance));
assertThat(allocations)
.collect(Collectors.toMap(GenesisAllocation::getAddress, Function.identity()));
assertThat(allocations.keySet())
.containsOnly(
entry("fe3b557e8fb62b89f4916b721be55ceb828dbd73", "0xad78ebc5ac6200000"),
entry("627306090abab3a6e1400e9345bc60c78a8bef57", "1000"),
entry("f17f52151ebef6c7334fad080c5704d77216b732", "90000000000000000000000"));
"fe3b557e8fb62b89f4916b721be55ceb828dbd73",
"627306090abab3a6e1400e9345bc60c78a8bef57",
"f17f52151ebef6c7334fad080c5704d77216b732");
final GenesisAllocation alloc1 = allocations.get("fe3b557e8fb62b89f4916b721be55ceb828dbd73");
final GenesisAllocation alloc2 = allocations.get("627306090abab3a6e1400e9345bc60c78a8bef57");
final GenesisAllocation alloc3 = allocations.get("f17f52151ebef6c7334fad080c5704d77216b732");
assertThat(alloc1.getBalance()).isEqualTo("0xad78ebc5ac6200000");
assertThat(alloc2.getBalance()).isEqualTo("1000");
assertThat(alloc3.getBalance()).isEqualTo("90000000000000000000000");
assertThat(alloc3.getStorage().size()).isEqualTo(2);
assertThat(
alloc3
.getStorage()
.get("0xc4c3a3f99b26e5e534b71d6f33ca6ea5c174decfb16dd7237c60eff9774ef4a4"))
.isEqualTo("0x937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0");
assertThat(
alloc3
.getStorage()
.get("0xc4c3a3f99b26e5e534b71d6f33ca6ea5c174decfb16dd7237c60eff9774ef4a3"))
.isEqualTo("0x6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012");
}
@Test

@ -17,10 +17,9 @@ import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Map;
import io.vertx.core.json.JsonObject;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
public class GenesisConfigOptionsTest {
@ -150,8 +149,10 @@ public class GenesisConfigOptionsTest {
assertThat(config.getHomesteadBlockNumber()).isEmpty();
}
private GenesisConfigOptions fromConfigOptions(final Map<String, Object> options) {
return GenesisConfigFile.fromConfig(new JsonObject(Collections.singletonMap("config", options)))
.getConfigOptions();
private GenesisConfigOptions fromConfigOptions(final Map<String, Object> configOptions) {
final ObjectNode rootNode = JsonUtil.createEmptyObjectNode();
final ObjectNode options = JsonUtil.objectNodeFromMap(configOptions);
rootNode.set("config", options);
return GenesisConfigFile.fromConfig(rootNode).getConfigOptions();
}
}

@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.Map;
import io.vertx.core.json.JsonObject;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
public class IbftConfigOptionsTest {
@ -179,9 +179,11 @@ public class IbftConfigOptionsTest {
}
private IbftConfigOptions fromConfigOptions(final Map<String, Object> ibftConfigOptions) {
return GenesisConfigFile.fromConfig(
new JsonObject(singletonMap("config", singletonMap("ibft", ibftConfigOptions))))
.getConfigOptions()
.getIbftLegacyConfigOptions();
final ObjectNode rootNode = JsonUtil.createEmptyObjectNode();
final ObjectNode configNode = JsonUtil.createEmptyObjectNode();
final ObjectNode options = JsonUtil.objectNodeFromMap(ibftConfigOptions);
configNode.set("ibft", options);
rootNode.set("config", configNode);
return GenesisConfigFile.fromConfig(rootNode).getConfigOptions().getIbftLegacyConfigOptions();
}
}

@ -0,0 +1,545 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.TreeMap;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
public class JsonUtilTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
public void getLong_nonExistentKey() {
final ObjectNode node = mapper.createObjectNode();
final OptionalLong result = JsonUtil.getLong(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getLong_nullValue() {
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final OptionalLong result = JsonUtil.getLong(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getLong_validValue() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", Long.MAX_VALUE);
final OptionalLong result = JsonUtil.getLong(node, "test");
assertThat(result).hasValue(Long.MAX_VALUE);
}
@Test
public void getLong_overflowingValue() {
final String overflowingValue = Long.toString(Long.MAX_VALUE, 10) + "100";
final String jsonStr = "{\"test\": " + overflowingValue + " }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getLong(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot convert value to long: " + overflowingValue);
}
@Test
public void getLong_wrongType() {
final String jsonStr = "{\"test\": \"bla\" }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getLong(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected number value but got string");
}
@Test
public void getLong_nullValue_withDefault() {
final long defaultValue = 11;
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final long result = JsonUtil.getLong(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getLong_nonExistentKey_withDefault() {
final long defaultValue = 11;
final ObjectNode node = mapper.createObjectNode();
final long result = JsonUtil.getLong(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getLong_validValue_withDefault() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", Long.MAX_VALUE);
final long result = JsonUtil.getLong(node, "test", 11);
assertThat(result).isEqualTo(Long.MAX_VALUE);
}
@Test
public void getLong_overflowingValue_withDefault() {
final String overflowingValue = Long.toString(Long.MAX_VALUE, 10) + "100";
final String jsonStr = "{\"test\": " + overflowingValue + " }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getLong(rootNode, "test", 11))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot convert value to long: " + overflowingValue);
}
@Test
public void getLong_wrongType_withDefault() {
final String jsonStr = "{\"test\": \"bla\" }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getLong(rootNode, "test", 11))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected number value but got string");
}
@Test
public void getInt_nonExistentKey() {
final ObjectNode node = mapper.createObjectNode();
final OptionalInt result = JsonUtil.getInt(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getInt_nullValue() {
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final OptionalInt result = JsonUtil.getInt(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getInt_validValue() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", Integer.MAX_VALUE);
final OptionalInt result = JsonUtil.getInt(node, "test");
assertThat(result).hasValue(Integer.MAX_VALUE);
}
@Test
public void getInt_overflowingValue() {
final String overflowingValue = Integer.toString(Integer.MAX_VALUE, 10) + "100";
final String jsonStr = "{\"test\": " + overflowingValue + " }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getInt(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot convert value to integer: " + overflowingValue);
}
@Test
public void getInt_wrongType() {
final String jsonStr = "{\"test\": \"bla\" }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getInt(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected number value but got string");
}
@Test
public void getInt_nullValue_withDefault() {
final int defaultValue = 11;
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final int result = JsonUtil.getInt(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getInt_nonExistentKey_withDefault() {
final int defaultValue = 11;
final ObjectNode node = mapper.createObjectNode();
final int result = JsonUtil.getInt(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getInt_validValue_withDefault() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", Integer.MAX_VALUE);
final int result = JsonUtil.getInt(node, "test", 11);
assertThat(result).isEqualTo(Integer.MAX_VALUE);
}
@Test
public void getInt_overflowingValue_withDefault() {
final String overflowingValue = Integer.toString(Integer.MAX_VALUE, 10) + "100";
final String jsonStr = "{\"test\": " + overflowingValue + " }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getInt(rootNode, "test", 11))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Cannot convert value to integer: " + overflowingValue);
}
@Test
public void getInt_wrongType_withDefault() {
final String jsonStr = "{\"test\": \"bla\" }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getInt(rootNode, "test", 11))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected number value but got string");
}
@Test
public void getString_nonExistentKey() {
final ObjectNode node = mapper.createObjectNode();
final Optional<String> result = JsonUtil.getString(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getString_nullValue() {
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final Optional<String> result = JsonUtil.getString(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getString_validValue() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", "bla");
final Optional<String> result = JsonUtil.getString(node, "test");
assertThat(result).hasValue("bla");
}
@Test
public void getString_wrongType() {
final String jsonStr = "{\"test\": 123 }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getString(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected string value but got number");
}
@Test
public void getString_nullValue_withDefault() {
final String defaultValue = "bla";
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final String result = JsonUtil.getString(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getString_nonExistentKey_withDefault() {
final String defaultValue = "bla";
final ObjectNode node = mapper.createObjectNode();
final String result = JsonUtil.getString(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getString_validValue_withDefault() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", "bla");
final String result = JsonUtil.getString(node, "test", "11");
assertThat(result).isEqualTo("bla");
}
@Test
public void getValueAsString_nonExistentKey() {
final ObjectNode node = mapper.createObjectNode();
final Optional<String> result = JsonUtil.getValueAsString(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getValueAsString_nullValue() {
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final Optional<String> result = JsonUtil.getValueAsString(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getValueAsString_stringValue() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", "bla");
final Optional<String> result = JsonUtil.getValueAsString(node, "test");
assertThat(result).hasValue("bla");
}
@Test
public void getValueAsString_nonStringValue() {
final String jsonStr = "{\"test\": 123 }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<String> result = JsonUtil.getValueAsString(rootNode, "test");
assertThat(result).hasValue("123");
}
@Test
public void getValueAsString_nullValue_withDefault() {
final String defaultValue = "bla";
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final String result = JsonUtil.getValueAsString(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getValueAsString_nonExistentKey_withDefault() {
final String defaultValue = "bla";
final ObjectNode node = mapper.createObjectNode();
final String result = JsonUtil.getValueAsString(node, "test", defaultValue);
assertThat(result).isEqualTo(defaultValue);
}
@Test
public void getValueAsString_stringValue_withDefault() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", "bla");
final String result = JsonUtil.getValueAsString(node, "test", "11");
assertThat(result).isEqualTo("bla");
}
@Test
public void getValueAsString_nonStringValue_withDefault() {
final String jsonStr = "{\"test\": 123 }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final String result = JsonUtil.getValueAsString(rootNode, "test", "11");
assertThat(result).isEqualTo("123");
}
// Boolean
@Test
public void getBoolean_nonExistentKey() {
final ObjectNode node = mapper.createObjectNode();
final Optional<Boolean> result = JsonUtil.getBoolean(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getBoolean_nullValue() {
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final Optional<Boolean> result = JsonUtil.getBoolean(node, "test");
assertThat(result).isEmpty();
}
@Test
public void getBoolean_validValue() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", true);
final Optional<Boolean> result = JsonUtil.getBoolean(node, "test");
assertThat(result).hasValue(true);
}
@Test
public void getBoolean_wrongType() {
final String jsonStr = "{\"test\": 123 }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getBoolean(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected boolean value but got number");
}
@Test
public void getBoolean_nullValue_withDefault() {
final ObjectNode node = mapper.createObjectNode();
node.set("test", null);
final Boolean result = JsonUtil.getBoolean(node, "test", false);
assertThat(result).isEqualTo(false);
}
@Test
public void getBoolean_nonExistentKey_withDefault() {
final ObjectNode node = mapper.createObjectNode();
final Boolean result = JsonUtil.getBoolean(node, "test", true);
assertThat(result).isEqualTo(true);
}
@Test
public void getBoolean_validValue_withDefault() {
final ObjectNode node = mapper.createObjectNode();
node.put("test", false);
final Boolean result = JsonUtil.getBoolean(node, "test", true);
assertThat(result).isEqualTo(false);
}
@Test
public void getBoolean_wrongType_withDefault() {
final String jsonStr = "{\"test\": 123 }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getBoolean(rootNode, "test", true))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected boolean value but got number");
}
@Test
public void objectNodeFromMap() {
final Map<String, Object> map = new TreeMap<>();
map.put("a", 1);
map.put("b", 2);
final Map<String, Object> subMap = new TreeMap<>();
subMap.put("c", "bla");
subMap.put("d", 2L);
map.put("subtree", subMap);
ObjectNode node = JsonUtil.objectNodeFromMap(map);
assertThat(node.get("a").asInt()).isEqualTo(1);
assertThat(node.get("b").asInt()).isEqualTo(2);
assertThat(node.get("subtree").get("c").asText()).isEqualTo("bla");
assertThat(node.get("subtree").get("d").asLong()).isEqualTo(2L);
}
@Test
public void objectNodeFromString() {
final String jsonStr = "{\"a\":1, \"b\":2}";
final ObjectNode result = JsonUtil.objectNodeFromString(jsonStr);
assertThat(result.get("a").asInt()).isEqualTo(1);
assertThat(result.get("b").asInt()).isEqualTo(2);
}
@Test
public void objectNodeFromString_withComments_commentsDisabled() {
final String jsonStr = "// Comment\n{\"a\":1, \"b\":2}";
assertThatThrownBy(() -> JsonUtil.objectNodeFromString(jsonStr, false))
.hasCauseInstanceOf(JsonParseException.class)
.hasMessageContaining("Unexpected character ('/'");
}
@Test
public void objectNodeFromString_withComments_commentsEnabled() {
final String jsonStr = "// Comment\n{\"a\":1, \"b\":2}";
final ObjectNode result = JsonUtil.objectNodeFromString(jsonStr, true);
assertThat(result.get("a").asInt()).isEqualTo(1);
assertThat(result.get("b").asInt()).isEqualTo(2);
}
@Test
public void getJson() throws JsonProcessingException {
final String jsonStr = "{\"a\":1, \"b\":2}";
final ObjectNode objectNode = JsonUtil.objectNodeFromString(jsonStr);
final String resultUgly = JsonUtil.getJson(objectNode, false);
final String resultPretty = JsonUtil.getJson(objectNode, true);
assertThat(resultUgly).isEqualToIgnoringWhitespace(jsonStr);
assertThat(resultPretty).isEqualToIgnoringWhitespace(jsonStr);
// Pretty printed value should have more whitespace and contain returns
assertThat(resultPretty.length()).isGreaterThan(resultUgly.length());
assertThat(resultPretty).contains("\n");
assertThat(resultUgly).doesNotContain("\n");
}
@Test
public void getObjectNode_validValue() {
final String jsonStr = "{\"test\": {\"a\":1, \"b\":2} }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<ObjectNode> maybeTestNode = JsonUtil.getObjectNode(rootNode, "test");
assertThat(maybeTestNode).isNotEmpty();
final ObjectNode testNode = maybeTestNode.get();
assertThat(testNode.get("a").asInt()).isEqualTo(1);
assertThat(testNode.get("b").asInt()).isEqualTo(2);
}
@Test
public void getObjectNode_nullValue() {
final String jsonStr = "{\"test\": null }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<ObjectNode> maybeTestNode = JsonUtil.getObjectNode(rootNode, "test");
assertThat(maybeTestNode).isEmpty();
}
@Test
public void getObjectNode_nonExistentKey() {
final String jsonStr = "{}";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<ObjectNode> maybeTestNode = JsonUtil.getObjectNode(rootNode, "test");
assertThat(maybeTestNode).isEmpty();
}
@Test
public void getObjectNode_wrongNodeType() {
final String jsonStr = "{\"test\": \"abc\" }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getObjectNode(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected object value but got string");
}
@Test
public void getArrayNode_validValue() {
final String jsonStr = "{\"test\": [\"a\", \"b\"] }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<ArrayNode> maybeTestNode = JsonUtil.getArrayNode(rootNode, "test");
assertThat(maybeTestNode).isNotEmpty();
final ArrayNode testNode = maybeTestNode.get();
assertThat(testNode.get(0).asText()).isEqualTo("a");
assertThat(testNode.get(1).asText()).isEqualTo("b");
}
@Test
public void getArrayNode_nullValue() {
final String jsonStr = "{\"test\": null }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<ArrayNode> maybeTestNode = JsonUtil.getArrayNode(rootNode, "test");
assertThat(maybeTestNode).isEmpty();
}
@Test
public void getArrayNode_nonExistentKey() {
final String jsonStr = "{}";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
final Optional<ArrayNode> maybeTestNode = JsonUtil.getArrayNode(rootNode, "test");
assertThat(maybeTestNode).isEmpty();
}
@Test
public void getArrayNode_wrongNodeType() {
final String jsonStr = "{\"test\": \"abc\" }";
final ObjectNode rootNode = JsonUtil.objectNodeFromString(jsonStr);
assertThatThrownBy(() -> JsonUtil.getArrayNode(rootNode, "test"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Expected array value but got string");
}
}

@ -48,7 +48,6 @@ import java.util.Random;
import java.util.concurrent.Executors;
import com.google.common.collect.Lists;
import io.vertx.core.json.JsonObject;
import org.junit.Before;
import org.junit.Test;
@ -56,7 +55,7 @@ public class CliqueMinerExecutorTest {
private static final int EPOCH_LENGTH = 10;
private static final GenesisConfigOptions GENESIS_CONFIG_OPTIONS =
GenesisConfigFile.fromConfig(new JsonObject()).getConfigOptions();
GenesisConfigFile.fromConfig("{}").getConfigOptions();
private final KeyPair proposerKeyPair = KeyPair.generate();
private final Random random = new Random(21341234L);
private Address localAddress;

@ -242,7 +242,7 @@ public final class GenesisState {
final String hexNonce,
final String hexAddress,
final String balance,
final Map<String, Object> storage,
final Map<String, String> storage,
final String hexCode,
final String version) {
this.nonce = withNiceErrorMessage("nonce", hexNonce, GenesisState::parseUnsignedLong);
@ -264,14 +264,19 @@ public final class GenesisState {
return Wei.of(val);
}
private Map<UInt256, UInt256> parseStorage(final Map<String, Object> storage) {
private Map<UInt256, UInt256> parseStorage(final Map<String, String> storage) {
final Map<UInt256, UInt256> parsedStorage = new HashMap<>();
storage.forEach(
(key, value) ->
parsedStorage.put(
withNiceErrorMessage("storage key", key, UInt256::fromHexString),
withNiceErrorMessage(
"storage value", String.valueOf(value), UInt256::fromHexString)));
storage
.entrySet()
.forEach(
(entry) -> {
final UInt256 key =
withNiceErrorMessage("storage key", entry.getKey(), UInt256::fromHexString);
final UInt256 value =
withNiceErrorMessage("storage value", entry.getValue(), UInt256::fromHexString);
parsedStorage.put(key, value);
});
return parsedStorage;
}

@ -17,7 +17,6 @@ import tech.pegasys.pantheon.config.GenesisConfigFile;
import java.nio.charset.StandardCharsets;
import com.google.common.io.Resources;
import io.vertx.core.json.JsonObject;
import org.assertj.core.api.Assertions;
import org.junit.Test;
@ -47,18 +46,16 @@ public class MainnetProtocolScheduleTest {
@Test
public void shouldOnlyUseFrontierWhenEmptyJsonConfigIsUsed() {
final JsonObject json = new JsonObject("{}");
final ProtocolSchedule<Void> sched =
MainnetProtocolSchedule.fromConfig(GenesisConfigFile.fromConfig(json).getConfigOptions());
MainnetProtocolSchedule.fromConfig(GenesisConfigFile.fromConfig("{}").getConfigOptions());
Assertions.assertThat(sched.getByBlockNumber(1L).getName()).isEqualTo("Frontier");
Assertions.assertThat(sched.getByBlockNumber(Long.MAX_VALUE).getName()).isEqualTo("Frontier");
}
@Test
public void createFromConfigWithSettings() {
final JsonObject json =
new JsonObject(
"{\"config\": {\"homesteadBlock\": 2, \"daoForkBlock\": 3, \"eip150Block\": 14, \"eip158Block\": 15, \"byzantiumBlock\": 16, \"constantinopleBlock\": 18, \"constantinopleFixBlock\": 19, \"chainId\":1234}}");
final String json =
"{\"config\": {\"homesteadBlock\": 2, \"daoForkBlock\": 3, \"eip150Block\": 14, \"eip158Block\": 15, \"byzantiumBlock\": 16, \"constantinopleBlock\": 18, \"constantinopleFixBlock\": 19, \"chainId\":1234}}";
final ProtocolSchedule<Void> sched =
MainnetProtocolSchedule.fromConfig(GenesisConfigFile.fromConfig(json).getConfigOptions());
Assertions.assertThat(sched.getByBlockNumber(1).getName()).isEqualTo("Frontier");
@ -75,9 +72,8 @@ public class MainnetProtocolScheduleTest {
@Test
public void outOfOrderConstantinoplesFail() {
final JsonObject json =
new JsonObject(
"{\"config\": {\"homesteadBlock\": 2, \"daoForkBlock\": 3, \"eip150Block\": 14, \"eip158Block\": 15, \"byzantiumBlock\": 16, \"constantinopleBlock\": 18, \"constantinopleFixBlock\": 17, \"chainId\":1234}}");
final String json =
"{\"config\": {\"homesteadBlock\": 2, \"daoForkBlock\": 3, \"eip150Block\": 14, \"eip158Block\": 15, \"byzantiumBlock\": 16, \"constantinopleBlock\": 18, \"constantinopleFixBlock\": 17, \"chainId\":1234}}";
Assertions.assertThatExceptionOfType(RuntimeException.class)
.describedAs(
"Genesis Config Error: 'ConstantinopleFix' is scheduled for block 17 but it must be on or after block 18.")

@ -22,6 +22,7 @@ import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.create
import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryWorldStateArchive;
import tech.pegasys.pantheon.config.GenesisConfigFile;
import tech.pegasys.pantheon.config.JsonUtil;
import tech.pegasys.pantheon.ethereum.chain.GenesisState;
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain;
import tech.pegasys.pantheon.ethereum.core.Address;
@ -36,6 +37,7 @@ import tech.pegasys.pantheon.metrics.PantheonMetricCategory;
import java.io.IOException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.io.Resources;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -55,8 +57,10 @@ public class NodeSmartContractPermissioningControllerTest {
final String emptyContractFile =
Resources.toString(this.getClass().getResource(resourceName), UTF_8);
final ObjectNode jsonData = JsonUtil.objectNodeFromString(emptyContractFile, true);
final GenesisState genesisState =
GenesisState.fromConfig(GenesisConfigFile.fromConfig(emptyContractFile), protocolSchedule);
GenesisState.fromConfig(GenesisConfigFile.fromConfig(jsonData), protocolSchedule);
final MutableBlockchain blockchain = createInMemoryBlockchain(genesisState.getBlock());
final WorldStateArchive worldArchive = createInMemoryWorldStateArchive();

@ -21,6 +21,7 @@ import static tech.pegasys.pantheon.cli.subcommands.operator.OperatorSubCommand.
import tech.pegasys.pantheon.cli.PantheonCommand;
import tech.pegasys.pantheon.config.JsonGenesisConfigOptions;
import tech.pegasys.pantheon.config.JsonUtil;
import tech.pegasys.pantheon.consensus.ibft.IbftExtraData;
import tech.pegasys.pantheon.crypto.SECP256K1;
import tech.pegasys.pantheon.ethereum.core.Address;
@ -39,9 +40,10 @@ import java.util.List;
import java.util.Set;
import java.util.stream.IntStream;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.io.Resources;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import picocli.CommandLine.Command;
@ -129,10 +131,10 @@ public class OperatorSubCommand implements Runnable {
@ParentCommand
private OperatorSubCommand parentCommand; // Picocli injects reference to parent command
private JsonObject operatorConfig;
private JsonObject genesisConfig;
private JsonObject blockchainConfig;
private JsonObject nodesConfig;
private ObjectNode operatorConfig;
private ObjectNode genesisConfig;
private ObjectNode blockchainConfig;
private ObjectNode nodesConfig;
private boolean generateNodesKeys;
private List<Address> addressesForGenesisExtraData = new ArrayList<>();
private Path keysDirectory;
@ -171,19 +173,25 @@ public class OperatorSubCommand implements Runnable {
/** Imports public keys from input configuration. */
private void importPublicKeysFromConfig() {
LOG.info("Importing public keys from configuration.");
JsonArray keys = nodesConfig.getJsonArray("keys");
keys.stream().forEach(this::importPublicKey);
JsonUtil.getArrayNode(nodesConfig, "keys")
.ifPresent(keys -> keys.forEach(this::importPublicKey));
}
/**
* Imports a single public key.
*
* @param publicKeyObject The public key.
* @param publicKeyJson The public key.
*/
private void importPublicKey(final Object publicKeyObject) {
private void importPublicKey(final JsonNode publicKeyJson) {
if (publicKeyJson.getNodeType() != JsonNodeType.STRING) {
throw new IllegalArgumentException(
"Invalid key json of type: " + publicKeyJson.getNodeType());
}
String publicKeyText = publicKeyJson.asText();
try {
final SECP256K1.PublicKey publicKey =
SECP256K1.PublicKey.create(BytesValue.fromHexString((String) publicKeyObject));
SECP256K1.PublicKey.create(BytesValue.fromHexString(publicKeyText));
writeKeypair(publicKey, null);
LOG.info("Public key imported from configuration.({})", publicKey.toString());
} catch (IOException e) {
@ -193,7 +201,7 @@ public class OperatorSubCommand implements Runnable {
/** Generates nodes keypairs. */
private void generateNodesKeys() {
final int nodesCount = nodesConfig.getInteger("count");
final int nodesCount = JsonUtil.getInt(nodesConfig, "count", 0);
LOG.info("Generating {} nodes keys.", nodesCount);
IntStream.range(0, nodesCount).forEach(this::generateNodeKeypair);
}
@ -241,8 +249,9 @@ public class OperatorSubCommand implements Runnable {
* @throws IOException
*/
private void processExtraData() {
final ObjectNode configNode = JsonUtil.getObjectNode(genesisConfig, "config").orElse(null);
final JsonGenesisConfigOptions genesisConfigOptions =
JsonGenesisConfigOptions.fromJsonObject(genesisConfig.getJsonObject("config"));
JsonGenesisConfigOptions.fromJsonObject(configNode);
if (genesisConfigOptions.isIbft2()) {
LOG.info("Generating IBFT extra data.");
final String extraData =
@ -265,11 +274,18 @@ public class OperatorSubCommand implements Runnable {
private void parseConfig() throws IOException {
final String configString =
Resources.toString(configurationFile.toPath().toUri().toURL(), UTF_8);
operatorConfig = new JsonObject(configString);
genesisConfig = operatorConfig.getJsonObject("genesis");
blockchainConfig = operatorConfig.getJsonObject("blockchain");
nodesConfig = blockchainConfig.getJsonObject("nodes");
generateNodesKeys = nodesConfig.getBoolean("generate", false);
final ObjectNode root = JsonUtil.objectNodeFromString(configString);
operatorConfig = root;
genesisConfig =
JsonUtil.getObjectNode(operatorConfig, "genesis")
.orElse(JsonUtil.createEmptyObjectNode());
blockchainConfig =
JsonUtil.getObjectNode(operatorConfig, "blockchain")
.orElse(JsonUtil.createEmptyObjectNode());
nodesConfig =
JsonUtil.getObjectNode(blockchainConfig, "nodes")
.orElse(JsonUtil.createEmptyObjectNode());
generateNodesKeys = JsonUtil.getBoolean(nodesConfig, "generate", false);
}
/**
@ -301,11 +317,11 @@ public class OperatorSubCommand implements Runnable {
* @throws IOException
*/
private void writeGenesisFile(
final File directory, final String fileName, final JsonObject genesis) throws IOException {
final File directory, final String fileName, final ObjectNode genesis) throws IOException {
LOG.info("Writing genesis file.");
Files.write(
directory.toPath().resolve(fileName),
genesis.encodePrettily().getBytes(UTF_8),
JsonUtil.getJson(genesis).getBytes(UTF_8),
StandardOpenOption.CREATE_NEW);
}
}

@ -21,6 +21,7 @@ import static java.util.Arrays.stream;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.contentOf;
import static org.junit.Assert.assertTrue;
import static tech.pegasys.pantheon.cli.operator.OperatorSubCommandTest.Cmd.cmd;
@ -152,15 +153,36 @@ public class OperatorSubCommandTest extends CommandTestAbstract {
asList("key.pub", "priv.test"));
}
@Test(expected = CommandLine.ExecutionException.class)
public void shouldFailIfDuplicateFiles() throws IOException {
runCmdAndCheckOutput(
cmd("--private-key-file-name", "dup.test", "--public-key-file-name", "dup.test"),
"/operator/config_generate_keys.json",
tmpOutputDirectoryPath,
"genesis.json",
true,
asList("key.pub", "priv.test"));
@Test
public void shouldFailIfDuplicateFiles() {
assertThatThrownBy(
() ->
runCmdAndCheckOutput(
cmd(
"--private-key-file-name",
"dup.test",
"--public-key-file-name",
"dup.test"),
"/operator/config_generate_keys.json",
tmpOutputDirectoryPath,
"genesis.json",
true,
asList("key.pub", "priv.test")))
.isInstanceOf(CommandLine.ExecutionException.class);
}
@Test
public void shouldFailIfPublicKeysAreWrongType() {
assertThatThrownBy(
() ->
runCmdAndCheckOutput(
cmd(),
"/operator/config_import_keys_invalid_keys.json",
tmpOutputDirectoryPath,
"genesis.json",
false,
singletonList("key.pub")))
.isInstanceOf(CommandLine.ExecutionException.class);
}
@Test(expected = CommandLine.ExecutionException.class)

@ -0,0 +1,44 @@
{
"genesis": {
"config": {
"chainId": 2017,
"constantinoplefixblock": 0,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"ibft2": {
}
},
"nonce": "0x0",
"timestamp": "0x5b3c3d18",
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x47b760",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"coinbase": "0x0000000000000000000000000000000000000000",
"ibft2": {
"blockperiodseconds": 2,
"epochlength": 30000,
"requesttimeoutseconds": 10
},
"alloc": {
"24defc2d149861d3d245749b81fe0e6b28e04f31": {
"balance": "0x446c3b15f9926687d2c40534fdb564000000000000"
},
"2a813d7db3de19b07f92268b6d4125ed295cbe00": {
"balance": "0x446c3b15f9926687d2c40534fdb542000000000000"
}
}
},
"blockchain": {
"nodes": {
"keys": [
{"invalidObj": "0xb295c4242fb40c6e8ac7b831c916846050f191adc560b8098ba6ad513079571ec1be6e5e1a715857a13a91963097962e048c36c5863014b59e8f67ed3f667680"},
"0x6295c4242fb40c6e8ac7b831c916846050f191adc560b8098ba6ad513079571ec1be6e5e1a715857a13a91963097962e048c36c5863014b59e8f67ed3f667680"
]
}
}
}
Loading…
Cancel
Save