JsonBlockImporter.java
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.chainimport;
import org.hyperledger.besu.chainimport.internal.BlockData;
import org.hyperledger.besu.chainimport.internal.ChainData;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.PowAlgorithm;
import org.hyperledger.besu.controller.BesuController;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.core.Block;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockImporter;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.BlockImportResult;
import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode;
import org.hyperledger.besu.evm.worldstate.WorldState;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Tool for importing blocks with transactions from human-readable json. */
public class JsonBlockImporter {
private static final Logger LOG = LoggerFactory.getLogger(JsonBlockImporter.class);
private final ObjectMapper mapper;
private final BesuController controller;
/**
* Instantiates a new Json block importer.
*
* @param controller the controller
*/
public JsonBlockImporter(final BesuController controller) {
this.controller = controller;
mapper = new ObjectMapper();
// Jdk8Module allows us to easily parse {@code Optional} values from json
mapper.registerModule(new Jdk8Module());
// Ignore casing of properties
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
}
/**
* Import chain.
*
* @param chainJson the chain json
* @throws IOException the io exception
*/
public void importChain(final String chainJson) throws IOException {
warnIfDatabaseIsNotEmpty();
final ChainData chainData = mapper.readValue(chainJson, ChainData.class);
final List<Block> importedBlocks = new ArrayList<>();
for (final BlockData blockData : chainData.getBlocks()) {
final BlockHeader parentHeader = getParentHeader(blockData, importedBlocks);
final Block importedBlock = processBlockData(blockData, parentHeader);
importedBlocks.add(importedBlock);
}
this.warnIfImportedBlocksAreNotOnCanonicalChain(importedBlocks);
}
private Block processBlockData(final BlockData blockData, final BlockHeader parentHeader) {
LOG.info(
"Preparing to import block at height {} (parent: {})",
parentHeader.getNumber() + 1L,
parentHeader.getHash());
final WorldState worldState =
controller
.getProtocolContext()
.getWorldStateArchive()
.get(parentHeader.getStateRoot(), parentHeader.getHash())
.get();
final List<Transaction> transactions =
blockData.streamTransactions(worldState).collect(Collectors.toList());
final Block block = createBlock(blockData, parentHeader, transactions);
assertAllTransactionsIncluded(block, transactions);
importBlock(block);
return block;
}
private Block createBlock(
final BlockData blockData,
final BlockHeader parentHeader,
final List<Transaction> transactions) {
final MiningCoordinator miner = controller.getMiningCoordinator();
final GenesisConfigOptions genesisConfigOptions = controller.getGenesisConfigOptions();
setOptionalFields(miner, blockData, genesisConfigOptions);
// Some MiningCoordinator's (specific to consensus type) do not support block-level imports
return miner
.createBlock(parentHeader, transactions, Collections.emptyList())
.orElseThrow(
() ->
new IllegalArgumentException(
"Unable to create block using current consensus engine: "
+ genesisConfigOptions.getConsensusEngine()));
}
private void setOptionalFields(
final MiningCoordinator miner,
final BlockData blockData,
final GenesisConfigOptions genesisConfig) {
// Some fields can only be configured for ethash
if (genesisConfig.getPowAlgorithm() != PowAlgorithm.UNSUPPORTED) {
// For simplicity only set these for PoW consensus algorithms.
// Other consensus algorithms use these fields for special purposes or ignore them.
miner.setCoinbase(blockData.getCoinbase().orElse(Address.ZERO));
miner.setExtraData(blockData.getExtraData().orElse(Bytes.EMPTY));
} else if (blockData.getCoinbase().isPresent() || blockData.getExtraData().isPresent()) {
// Fail if these fields are set for non-ethash chains
final Stream.Builder<String> fields = Stream.builder();
blockData.getCoinbase().map((c) -> "coinbase").ifPresent(fields::add);
blockData.getExtraData().map((e) -> "extraData").ifPresent(fields::add);
final String fieldsList = fields.build().collect(Collectors.joining(", "));
throw new IllegalArgumentException(
"Some fields ("
+ fieldsList
+ ") are unsupported by the current consensus engine: "
+ genesisConfig.getConsensusEngine());
}
}
private void importBlock(final Block block) {
final BlockImporter importer =
controller.getProtocolSchedule().getByBlockHeader(block.getHeader()).getBlockImporter();
final BlockImportResult importResult =
importer.importBlock(controller.getProtocolContext(), block, HeaderValidationMode.NONE);
if (importResult.isImported()) {
LOG.info(
"Successfully created and imported block at height {} ({})",
block.getHeader().getNumber(),
block.getHash());
} else {
throw new IllegalStateException(
"Newly created block " + block.getHeader().getNumber() + " failed validation.");
}
}
private void assertAllTransactionsIncluded(
final Block block, final List<Transaction> transactions) {
if (transactions.size() != block.getBody().getTransactions().size()) {
final int missingTransactions =
transactions.size() - block.getBody().getTransactions().size();
throw new IllegalStateException(
"Unable to create block. "
+ missingTransactions
+ " transaction(s) were found to be invalid.");
}
}
private void warnIfDatabaseIsNotEmpty() {
final long chainHeight =
controller.getProtocolContext().getBlockchain().getChainHead().getHeight();
if (chainHeight > BlockHeader.GENESIS_BLOCK_NUMBER) {
LOG.warn(
"Importing to a non-empty database with chain height {}. This may cause imported blocks to be considered non-canonical.",
chainHeight);
}
}
private void warnIfImportedBlocksAreNotOnCanonicalChain(final List<Block> importedBlocks) {
final List<BlockHeader> nonCanonicalHeaders =
importedBlocks.stream()
.map(Block::getHeader)
.filter(
header ->
controller
.getProtocolContext()
.getBlockchain()
.getBlockHeader(header.getNumber())
.map(c -> !c.equals(header))
.orElse(true))
.collect(Collectors.toList());
if (nonCanonicalHeaders.size() > 0) {
final String blocksString =
nonCanonicalHeaders.stream()
.map(h -> "#" + h.getNumber() + " (" + h.getHash() + ")")
.collect(Collectors.joining(", "));
LOG.warn(
"{} / {} imported blocks are not on the canonical chain: {}",
nonCanonicalHeaders.size(),
importedBlocks.size(),
blocksString);
}
}
private BlockHeader getParentHeader(final BlockData blockData, final List<Block> importedBlocks) {
if (blockData.getParentHash().isPresent()) {
final Hash parentHash = blockData.getParentHash().get();
return controller
.getProtocolContext()
.getBlockchain()
.getBlockHeader(parentHash)
.orElseThrow(
() -> new IllegalArgumentException("Unable to locate block parent at " + parentHash));
}
if (importedBlocks.size() > 0 && blockData.getNumber().isPresent()) {
final long targetParentBlockNumber = blockData.getNumber().get() - 1L;
final Optional<BlockHeader> maybeHeader =
importedBlocks.stream()
.map(Block::getHeader)
.filter(h -> h.getNumber() == targetParentBlockNumber)
.findFirst();
if (maybeHeader.isPresent()) {
return maybeHeader.get();
}
}
final long blockNumber;
if (blockData.getNumber().isPresent()) {
blockNumber = blockData.getNumber().get() - 1L;
} else if (importedBlocks.size() > 0) {
// If there is no number or hash, import blocks in order
blockNumber = importedBlocks.get(importedBlocks.size() - 1).getHeader().getNumber();
} else {
blockNumber = BlockHeader.GENESIS_BLOCK_NUMBER;
}
if (blockNumber < BlockHeader.GENESIS_BLOCK_NUMBER) {
throw new IllegalArgumentException("Invalid block number: " + (blockNumber + 1));
}
return controller
.getProtocolContext()
.getBlockchain()
.getBlockHeader(blockNumber)
.orElseThrow(
() -> new IllegalArgumentException("Unable to locate block parent at " + blockNumber));
}
}