| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE |
| * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file |
| * to You 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 org.apache.tuweni.eth.genesis; |
| |
| import org.apache.tuweni.bytes.Bytes; |
| import org.apache.tuweni.concurrent.AsyncCompletion; |
| import org.apache.tuweni.eth.AccountState; |
| import org.apache.tuweni.eth.Address; |
| import org.apache.tuweni.eth.Block; |
| import org.apache.tuweni.eth.BlockBody; |
| import org.apache.tuweni.eth.BlockHeader; |
| import org.apache.tuweni.eth.Hash; |
| import org.apache.tuweni.rlp.RLP; |
| import org.apache.tuweni.trie.MerklePatriciaTrie; |
| import org.apache.tuweni.units.bigints.UInt256; |
| import org.apache.tuweni.units.bigints.UInt64; |
| import org.apache.tuweni.units.ethereum.Gas; |
| import org.apache.tuweni.units.ethereum.Wei; |
| |
| import java.io.IOException; |
| import java.math.BigInteger; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| import com.fasterxml.jackson.core.JsonFactory; |
| import com.fasterxml.jackson.core.JsonParser; |
| import com.fasterxml.jackson.core.JsonToken; |
| |
| /** |
| * Utility to read genesis config files and translate them to a block. |
| */ |
| public class GenesisFile { |
| |
| private final Bytes nonce; |
| private final UInt256 difficulty; |
| private final Hash mixhash; |
| private final Address coinbase; |
| private final Instant timestamp; |
| private final Bytes extraData; |
| private final Gas gasLimit; |
| private final Map<Address, Wei> allocs; |
| private final int chainId; |
| private final List<Long> forks; |
| |
| |
| public GenesisFile( |
| String nonce, |
| String difficulty, |
| String mixhash, |
| String coinbase, |
| String timestamp, |
| String extraData, |
| String gasLimit, |
| String parentHash, |
| Map<String, String> allocs, |
| int chainId, |
| List<Long> forks) { |
| if (nonce == null) { |
| throw new IllegalArgumentException("nonce must be provided"); |
| } |
| if (difficulty == null) { |
| throw new IllegalArgumentException("difficulty must be provided"); |
| } |
| if (mixhash == null) { |
| throw new IllegalArgumentException("mixhash must be provided"); |
| } |
| if (coinbase == null) { |
| throw new IllegalArgumentException("coinbase must be provided"); |
| } |
| if (timestamp == null) { |
| throw new IllegalArgumentException("timestamp must be provided"); |
| } |
| if (extraData == null) { |
| throw new IllegalArgumentException("extraData must be provided"); |
| } |
| if (gasLimit == null) { |
| throw new IllegalArgumentException("gasLimit must be provided"); |
| } |
| this.nonce = Bytes.fromHexString(nonce); |
| this.difficulty = UInt256.fromHexString(difficulty); |
| this.mixhash = Hash.fromHexString(mixhash); |
| this.coinbase = Address.fromHexString(coinbase); |
| this.timestamp = "0x0".equals(timestamp) ? Instant.ofEpochSecond(0) |
| : Instant.ofEpochSecond(Bytes.fromHexString(timestamp).toLong()); |
| this.extraData = Bytes.fromHexString(extraData); |
| this.gasLimit = Gas.valueOf(Bytes.fromHexString(gasLimit).toLong()); |
| this.allocs = new HashMap<>(); |
| for (Map.Entry<String, String> entry : allocs.entrySet()) { |
| Address addr = null; |
| try { |
| addr = Address.fromHexString(entry.getKey()); |
| } catch (IllegalArgumentException e) { |
| throw new IllegalArgumentException("Invalid address " + entry.getKey(), e); |
| } |
| Wei value = null; |
| if (entry.getValue().startsWith("0x")) { |
| try { |
| value = Wei.valueOf(UInt256.fromHexString(entry.getValue())); |
| } catch (IllegalArgumentException e) { |
| throw new IllegalArgumentException("Invalid balance " + entry.getValue(), e); |
| } |
| } else { |
| try { |
| value = Wei.valueOf(UInt256.valueOf(new BigInteger(entry.getValue()))); |
| } catch (IllegalArgumentException e) { |
| throw new IllegalArgumentException("Invalid balance " + entry.getValue(), e); |
| } |
| } |
| this.allocs.put(addr, value); |
| } |
| this.chainId = chainId; |
| this.forks = forks; |
| } |
| |
| public static GenesisFile read(byte[] contents) throws IOException { |
| JsonFactory factory = new JsonFactory(); |
| JsonParser parser = factory.createParser(contents); |
| |
| int chainId = 0; |
| String nonce = null; |
| String difficulty = null; |
| String mixhash = null; |
| String coinbase = null; |
| String timestamp = null; |
| String extraData = null; |
| String gasLimit = null; |
| String parentHash = null; |
| Map<String, String> allocs = null; |
| Set<Long> collectedForks = new TreeSet<>(); |
| while (!parser.isClosed()) { |
| JsonToken jsonToken = parser.nextToken(); |
| if (JsonToken.FIELD_NAME.equals(jsonToken)) { |
| String fieldName = parser.getCurrentName(); |
| |
| parser.nextToken(); |
| |
| if ("nonce".equalsIgnoreCase(fieldName)) { |
| nonce = parser.getValueAsString(); |
| } else if ("difficulty".equalsIgnoreCase(fieldName)) { |
| difficulty = parser.getValueAsString(); |
| } else if ("mixHash".equalsIgnoreCase(fieldName)) { |
| mixhash = parser.getValueAsString(); |
| } else if ("coinbase".equalsIgnoreCase(fieldName)) { |
| coinbase = parser.getValueAsString(); |
| } else if ("gasLimit".equalsIgnoreCase(fieldName)) { |
| gasLimit = parser.getValueAsString(); |
| } else if ("timestamp".equalsIgnoreCase(fieldName)) { |
| timestamp = parser.getValueAsString(); |
| } else if ("extraData".equalsIgnoreCase(fieldName)) { |
| extraData = parser.getValueAsString(); |
| } else if ("parentHash".equalsIgnoreCase(fieldName)) { |
| parentHash = parser.getValueAsString(); |
| } else if ("alloc".equalsIgnoreCase(fieldName)) { |
| allocs = readAlloc(parser); |
| } else if ("chainId".equalsIgnoreCase(fieldName)) { |
| chainId = parser.getValueAsInt(); |
| } else if (fieldName.contains("Block")) { |
| collectedForks.add(parser.getValueAsLong()); |
| } |
| } |
| } |
| List<Long> forks = new ArrayList<>(collectedForks); |
| Collections.sort(forks); |
| return new GenesisFile( |
| nonce, |
| difficulty, |
| mixhash, |
| coinbase, |
| timestamp, |
| extraData, |
| gasLimit, |
| parentHash, |
| allocs, |
| chainId, |
| forks); |
| } |
| |
| private static Map<String, String> readAlloc(JsonParser parser) throws IOException { |
| Map<String, String> allocs = new HashMap<>(); |
| String name = null; |
| String value = null; |
| int depth = 1; |
| while (!parser.isClosed()) { |
| JsonToken jsonToken = parser.nextToken(); |
| if (JsonToken.FIELD_NAME.equals(jsonToken)) { |
| String fieldName = parser.getCurrentName(); |
| if ("balance".equals(fieldName)) { |
| parser.nextToken(); |
| value = parser.getValueAsString(); |
| allocs.put(name, value); |
| name = null; |
| } else { |
| if (depth == 1) { |
| name = parser.getValueAsString(); |
| } |
| } |
| } else if (JsonToken.END_OBJECT.equals(jsonToken)) { |
| depth--; |
| if (depth == 0) { |
| return allocs; |
| } |
| } else if (JsonToken.START_OBJECT.equals(jsonToken)) { |
| depth++; |
| } |
| } |
| return allocs; |
| } |
| |
| |
| public Block toBlock() { |
| Hash emptyListHash = Hash.hash(RLP.encodeList(writer -> { |
| })); |
| Hash emptyHash = Hash.hash(RLP.encode(writer -> { |
| writer.writeValue(Bytes.EMPTY); |
| })); |
| Hash empty = Hash.hash(Bytes.EMPTY); |
| MerklePatriciaTrie<AccountState> stateTree = new MerklePatriciaTrie<>(AccountState::toBytes); |
| |
| List<AsyncCompletion> futures = new ArrayList<>(); |
| for (Map.Entry<Address, Wei> entry : allocs.entrySet()) { |
| AccountState accountState = new AccountState(UInt256.ZERO, entry.getValue(), emptyHash, empty); |
| futures.add(stateTree.putAsync(Hash.hash(entry.getKey()), accountState)); |
| } |
| try { |
| AsyncCompletion.allOf(futures).join(10, TimeUnit.SECONDS); |
| } catch (InterruptedException | TimeoutException e) { |
| throw new RuntimeException("Creating the account states took more than 10 seconds."); |
| } |
| return new Block( |
| new BlockHeader( |
| null, |
| emptyListHash, |
| coinbase, |
| Hash.fromBytes(stateTree.rootHash()), |
| emptyHash, |
| emptyHash, |
| Bytes.wrap(new byte[256]), |
| difficulty, |
| UInt256.ZERO, |
| gasLimit, |
| Gas.valueOf(0L), |
| timestamp, |
| extraData, |
| mixhash, |
| UInt64.fromBytes(nonce)), |
| new BlockBody(new ArrayList<>(), new ArrayList<>())); |
| } |
| |
| public Map<Address, Wei> getAllocations() { |
| return allocs; |
| } |
| |
| public List<Long> getForks() { |
| return forks; |
| } |
| |
| public int getChainId() { |
| return chainId; |
| } |
| } |