From 6998e6539211872024ea630959c8498dc53cc58e Mon Sep 17 00:00:00 2001 From: mbaxter Date: Mon, 12 Aug 2019 13:01:57 -0400 Subject: [PATCH] [PIE-1809] Add import chain json utility (#1832) --- .../pantheon/config/GenesisConfigOptions.java | 2 + .../config/JsonGenesisConfigOptions.java | 16 +- .../config/StubGenesisConfigOptions.java | 5 + .../config/GenesisConfigOptionsTest.java | 11 + .../blockcreation/CliqueBlockMiner.java | 4 +- .../blockcreation/CliqueMinerExecutor.java | 55 ++- .../blockcreation/IbftMiningCoordinator.java | 13 + .../blockcreation/AbstractBlockCreator.java | 28 +- .../blockcreation/AbstractMinerExecutor.java | 2 + .../AbstractMiningCoordinator.java | 12 + .../ethereum/blockcreation/BlockCreator.java | 7 + .../ethereum/blockcreation/BlockMiner.java | 37 +- .../BlockTransactionSelector.java | 11 + .../blockcreation/EthHashBlockMiner.java | 9 +- .../blockcreation/EthHashMinerExecutor.java | 50 +- .../blockcreation/MiningCoordinator.java | 17 + .../blockcreation/BlockMinerTest.java | 10 +- .../pantheon/ethereum/core/Address.java | 5 + pantheon/build.gradle | 2 + .../java/tech/pegasys/pantheon/Pantheon.java | 2 + .../pantheon/chainimport/BlockData.java | 90 ++++ .../pantheon/chainimport/ChainData.java | 32 ++ .../pantheon/chainimport/ChainImporter.java | 248 ++++++++++ .../pantheon/chainimport/TransactionData.java | 73 +++ .../pegasys/pantheon/cli/PantheonCommand.java | 26 +- .../cli/subcommands/blocks/BlockFormat.java | 18 + .../subcommands/blocks/BlocksSubCommand.java | 93 +++- .../management/RestfulRouteBuilder.java | 152 ------- .../chainimport/ChainImporterTest.java | 429 ++++++++++++++++++ .../pantheon/cli/CommandTestAbstract.java | 11 + .../blocks/BlocksSubCommandTest.java} | 61 ++- .../blocks-import-invalid-bad-parent.json | 16 + .../clique/blocks-import-invalid-bad-tx.json | 16 + .../clique/blocks-import-special-fields.json | 17 + .../clique/blocks-import-valid-addendum.json | 16 + ...cks-import-valid-no-block-identifiers.json | 44 ++ .../clique/blocks-import-valid.json | 48 ++ .../pantheon/chainimport/clique/genesis.json | 45 ++ .../blocks-import-invalid-bad-parent.json | 16 + .../ethash/blocks-import-invalid-bad-tx.json | 16 + .../ethash/blocks-import-special-fields.json | 17 + .../ethash/blocks-import-valid-addendum.json | 16 + ...cks-import-valid-no-block-identifiers.json | 48 ++ .../ethash/blocks-import-valid.json | 52 +++ .../pantheon/chainimport/ethash/genesis.json | 44 ++ .../chainimport/unsupported/genesis.json | 46 ++ 46 files changed, 1753 insertions(+), 235 deletions(-) create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/chainimport/BlockData.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainData.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainImporter.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/chainimport/TransactionData.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlockFormat.java delete mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/management/RestfulRouteBuilder.java create mode 100644 pantheon/src/test/java/tech/pegasys/pantheon/chainimport/ChainImporterTest.java rename pantheon/src/test/java/tech/pegasys/pantheon/cli/{BlockSubCommandTest.java => subcommands/blocks/BlocksSubCommandTest.java} (84%) create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-parent.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-tx.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-special-fields.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-addendum.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-no-block-identifiers.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/genesis.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-parent.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-tx.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-special-fields.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-addendum.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-no-block-identifiers.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/genesis.json create mode 100644 pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/unsupported/genesis.json diff --git a/config/src/main/java/tech/pegasys/pantheon/config/GenesisConfigOptions.java b/config/src/main/java/tech/pegasys/pantheon/config/GenesisConfigOptions.java index 5d8476f6c1..e860c60477 100644 --- a/config/src/main/java/tech/pegasys/pantheon/config/GenesisConfigOptions.java +++ b/config/src/main/java/tech/pegasys/pantheon/config/GenesisConfigOptions.java @@ -28,6 +28,8 @@ public interface GenesisConfigOptions { boolean isClique(); + String getConsensusEngine(); + IbftConfigOptions getIbftLegacyConfigOptions(); CliqueConfigOptions getCliqueConfigOptions(); diff --git a/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java b/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java index 48011227ad..65c2023697 100644 --- a/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java +++ b/config/src/main/java/tech/pegasys/pantheon/config/JsonGenesisConfigOptions.java @@ -24,7 +24,6 @@ import com.google.common.collect.ImmutableMap; public class JsonGenesisConfigOptions implements GenesisConfigOptions { - private static final String ETHASH_CONFIG_KEY = "ethash"; private static final String IBFT_LEGACY_CONFIG_KEY = "ibft"; private static final String IBFT2_CONFIG_KEY = "ibft2"; @@ -39,6 +38,21 @@ public static JsonGenesisConfigOptions fromJsonObject(final ObjectNode configRoo this.configRoot = isNull(maybeConfig) ? JsonUtil.createEmptyObjectNode() : maybeConfig; } + @Override + public String getConsensusEngine() { + if (isEthHash()) { + return ETHASH_CONFIG_KEY; + } else if (isIbft2()) { + return IBFT2_CONFIG_KEY; + } else if (isIbftLegacy()) { + return IBFT_LEGACY_CONFIG_KEY; + } else if (isClique()) { + return CLIQUE_CONFIG_KEY; + } else { + return "unknown"; + } + } + @Override public boolean isEthHash() { return configRoot.has(ETHASH_CONFIG_KEY); diff --git a/config/src/test-support/java/tech/pegasys/pantheon/config/StubGenesisConfigOptions.java b/config/src/test-support/java/tech/pegasys/pantheon/config/StubGenesisConfigOptions.java index 8c97b29fb1..cda81db41a 100644 --- a/config/src/test-support/java/tech/pegasys/pantheon/config/StubGenesisConfigOptions.java +++ b/config/src/test-support/java/tech/pegasys/pantheon/config/StubGenesisConfigOptions.java @@ -34,6 +34,11 @@ public class StubGenesisConfigOptions implements GenesisConfigOptions { private OptionalInt contractSizeLimit = OptionalInt.empty(); private OptionalInt stackSizeLimit = OptionalInt.empty(); + @Override + public String getConsensusEngine() { + return "ethash"; + } + @Override public boolean isEthHash() { return true; diff --git a/config/src/test/java/tech/pegasys/pantheon/config/GenesisConfigOptionsTest.java b/config/src/test/java/tech/pegasys/pantheon/config/GenesisConfigOptionsTest.java index c4c7cae437..cddb330a6a 100644 --- a/config/src/test/java/tech/pegasys/pantheon/config/GenesisConfigOptionsTest.java +++ b/config/src/test/java/tech/pegasys/pantheon/config/GenesisConfigOptionsTest.java @@ -28,6 +28,7 @@ public class GenesisConfigOptionsTest { public void shouldUseEthHashWhenEthHashInConfig() { final GenesisConfigOptions config = fromConfigOptions(singletonMap("ethash", emptyMap())); assertThat(config.isEthHash()).isTrue(); + assertThat(config.getConsensusEngine()).isEqualTo("ethash"); } @Test @@ -41,6 +42,7 @@ public void shouldUseIbftLegacyWhenIbftInConfig() { final GenesisConfigOptions config = fromConfigOptions(singletonMap("ibft", emptyMap())); assertThat(config.isIbftLegacy()).isTrue(); assertThat(config.getIbftLegacyConfigOptions()).isNotSameAs(IbftConfigOptions.DEFAULT); + assertThat(config.getConsensusEngine()).isEqualTo("ibft"); } @Test @@ -50,11 +52,20 @@ public void shouldNotUseIbftLegacyIfIbftNotPresent() { assertThat(config.getIbftLegacyConfigOptions()).isSameAs(IbftConfigOptions.DEFAULT); } + @Test + public void shouldUseIbft2WhenIbft2InConfig() { + final GenesisConfigOptions config = fromConfigOptions(singletonMap("ibft2", emptyMap())); + assertThat(config.isIbftLegacy()).isFalse(); + assertThat(config.isIbft2()).isTrue(); + assertThat(config.getConsensusEngine()).isEqualTo("ibft2"); + } + @Test public void shouldUseCliqueWhenCliqueInConfig() { final GenesisConfigOptions config = fromConfigOptions(singletonMap("clique", emptyMap())); assertThat(config.isClique()).isTrue(); assertThat(config.getCliqueConfigOptions()).isNotSameAs(CliqueConfigOptions.DEFAULT); + assertThat(config.getConsensusEngine()).isEqualTo("clique"); } @Test diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueBlockMiner.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueBlockMiner.java index 210d0ba9b9..533a89680c 100644 --- a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueBlockMiner.java +++ b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueBlockMiner.java @@ -23,12 +23,14 @@ import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.util.Subscribers; +import java.util.function.Function; + public class CliqueBlockMiner extends BlockMiner { private final Address localAddress; public CliqueBlockMiner( - final CliqueBlockCreator blockCreator, + final Function blockCreator, final ProtocolSchedule protocolSchedule, final ProtocolContext protocolContext, final Subscribers observers, diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueMinerExecutor.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueMinerExecutor.java index da44f54b60..96514f3c24 100644 --- a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueMinerExecutor.java +++ b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/blockcreation/CliqueMinerExecutor.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutorService; +import java.util.function.Function; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; @@ -68,32 +69,42 @@ public CliqueMinerExecutor( @Override public CliqueBlockMiner startAsyncMining( final Subscribers observers, final BlockHeader parentHeader) { - final CliqueBlockCreator blockCreator = - new CliqueBlockCreator( - localAddress, // TOOD(tmm): This can be removed (used for voting not coinbase). - this::calculateExtraData, - pendingTransactions, - protocolContext, - protocolSchedule, - (gasLimit) -> gasLimit, - nodeKeys, - minTransactionGasPrice, - parentHeader, - epochManager); - - final CliqueBlockMiner currentRunningMiner = - new CliqueBlockMiner( - blockCreator, - protocolSchedule, - protocolContext, - observers, - blockScheduler, - parentHeader, - localAddress); + final CliqueBlockMiner currentRunningMiner = createMiner(observers, parentHeader); executorService.execute(currentRunningMiner); return currentRunningMiner; } + @Override + public CliqueBlockMiner createMiner(final BlockHeader parentHeader) { + return createMiner(Subscribers.none(), parentHeader); + } + + private CliqueBlockMiner createMiner( + final Subscribers observers, final BlockHeader parentHeader) { + final Function blockCreator = + (header) -> + new CliqueBlockCreator( + localAddress, // TOOD(tmm): This can be removed (used for voting not coinbase). + this::calculateExtraData, + pendingTransactions, + protocolContext, + protocolSchedule, + (gasLimit) -> gasLimit, + nodeKeys, + minTransactionGasPrice, + header, + epochManager); + + return new CliqueBlockMiner( + blockCreator, + protocolSchedule, + protocolContext, + observers, + blockScheduler, + parentHeader, + localAddress); + } + @Override public Optional
getCoinbase() { return Optional.of(localAddress); diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/blockcreation/IbftMiningCoordinator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/blockcreation/IbftMiningCoordinator.java index b67f93e9f5..e56054a608 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/blockcreation/IbftMiningCoordinator.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/blockcreation/IbftMiningCoordinator.java @@ -22,9 +22,13 @@ import tech.pegasys.pantheon.ethereum.chain.BlockAddedObserver; import tech.pegasys.pantheon.ethereum.chain.Blockchain; import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.List; import java.util.Optional; import org.apache.logging.log4j.Logger; @@ -78,6 +82,15 @@ public Optional
getCoinbase() { return Optional.of(blockCreatorFactory.getLocalAddress()); } + @Override + public Optional createBlock( + final BlockHeader parentHeader, + final List transactions, + final List ommers) { + // One-off block creation has not been implemented + return Optional.empty(); + } + @Override public void onBlockAdded(final BlockAddedEvent event, final Blockchain blockchain) { if (event.isNewCanonicalHead()) { diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java index 8c02208140..837f13631d 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java @@ -24,6 +24,7 @@ import tech.pegasys.pantheon.ethereum.core.MutableWorldState; import tech.pegasys.pantheon.ethereum.core.ProcessableBlockHeader; import tech.pegasys.pantheon.ethereum.core.SealableBlockHeader; +import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.ethereum.core.WorldUpdater; import tech.pegasys.pantheon.ethereum.eth.transactions.PendingTransactions; @@ -39,6 +40,7 @@ import java.math.BigInteger; import java.util.List; +import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -112,6 +114,19 @@ public AbstractBlockCreator( */ @Override public Block createBlock(final long timestamp) { + return createBlock(Optional.empty(), Optional.empty(), timestamp); + } + + @Override + public Block createBlock( + final List transactions, final List ommers, final long timestamp) { + return createBlock(Optional.of(transactions), Optional.of(ommers), timestamp); + } + + private Block createBlock( + final Optional> maybeTransactions, + final Optional> maybeOmmers, + final long timestamp) { try { final ProcessableBlockHeader processableBlockHeader = createPendingBlockHeader(timestamp); @@ -121,12 +136,12 @@ public Block createBlock(final long timestamp) { throwIfStopped(); - final List ommers = selectOmmers(); + final List ommers = maybeOmmers.orElse(selectOmmers()); throwIfStopped(); final BlockTransactionSelector.TransactionSelectionResults transactionResults = - selectTransactions(processableBlockHeader, disposableWorldState); + selectTransactions(processableBlockHeader, disposableWorldState, maybeTransactions); throwIfStopped(); @@ -174,7 +189,8 @@ public Block createBlock(final long timestamp) { private BlockTransactionSelector.TransactionSelectionResults selectTransactions( final ProcessableBlockHeader processableBlockHeader, - final MutableWorldState disposableWorldState) + final MutableWorldState disposableWorldState, + final Optional> transactions) throws RuntimeException { final long blockNumber = processableBlockHeader.getNumber(); @@ -196,7 +212,11 @@ private BlockTransactionSelector.TransactionSelectionResults selectTransactions( isCancelled::get, miningBeneficiary); - return selector.buildTransactionListForBlock(); + if (transactions.isPresent()) { + return selector.evaluateTransactions(transactions.get()); + } else { + return selector.buildTransactionListForBlock(); + } } private MutableWorldState duplicateWorldStateAtParent() { diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMinerExecutor.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMinerExecutor.java index 0439bccaab..f92b660481 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMinerExecutor.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMinerExecutor.java @@ -57,6 +57,8 @@ public AbstractMinerExecutor( public abstract M startAsyncMining( final Subscribers observers, final BlockHeader parentHeader); + public abstract M createMiner(final BlockHeader parentHeader); + public void setExtraData(final BytesValue extraData) { this.extraData = extraData.copy(); } diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMiningCoordinator.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMiningCoordinator.java index aab702e208..9b90972e92 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMiningCoordinator.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/AbstractMiningCoordinator.java @@ -19,12 +19,15 @@ import tech.pegasys.pantheon.ethereum.chain.Blockchain; import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver; import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.ethereum.eth.sync.state.SyncState; import tech.pegasys.pantheon.util.Subscribers; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.List; import java.util.Optional; import org.apache.logging.log4j.Logger; @@ -53,6 +56,15 @@ public AbstractMiningCoordinator( syncState.addInSyncListener(this::inSyncChanged); } + @Override + public Optional createBlock( + final BlockHeader parentHeader, + final List transactions, + final List ommers) { + M miner = executor.createMiner(parentHeader); + return Optional.of(miner.createBlock(parentHeader, transactions, ommers)); + } + @Override public void enable() { synchronized (this) { diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockCreator.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockCreator.java index 698dc71b7b..fba44b632d 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockCreator.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockCreator.java @@ -13,7 +13,14 @@ package tech.pegasys.pantheon.ethereum.blockcreation; import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Transaction; + +import java.util.List; public interface BlockCreator { Block createBlock(final long timestamp); + + Block createBlock( + final List transactions, final List ommers, final long timestamp); } diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMiner.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMiner.java index c16861f9fc..70f14ad492 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMiner.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMiner.java @@ -17,12 +17,15 @@ import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.BlockHeader; import tech.pegasys.pantheon.ethereum.core.BlockImporter; +import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; import tech.pegasys.pantheon.util.Subscribers; +import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import com.google.common.base.Stopwatch; import org.apache.logging.log4j.LogManager; @@ -42,7 +45,9 @@ public class BlockMiner> implements Runnabl private static final Logger LOG = LogManager.getLogger(); - protected final M blockCreator; + protected final Function blockCreatorFactory; + protected final M minerBlockCreator; + protected final ProtocolContext protocolContext; protected final BlockHeader parentHeader; @@ -51,13 +56,14 @@ public class BlockMiner> implements Runnabl private final AbstractBlockScheduler scheduler; public BlockMiner( - final M blockCreator, + final Function blockCreatorFactory, final ProtocolSchedule protocolSchedule, final ProtocolContext protocolContext, final Subscribers observers, final AbstractBlockScheduler scheduler, final BlockHeader parentHeader) { - this.blockCreator = blockCreator; + this.blockCreatorFactory = blockCreatorFactory; + this.minerBlockCreator = blockCreatorFactory.apply(parentHeader); this.protocolContext = protocolContext; this.protocolSchedule = protocolSchedule; this.observers = observers; @@ -69,7 +75,7 @@ public BlockMiner( public void run() { boolean blockMined = false; - while (!blockMined && !blockCreator.isCancelled()) { + while (!blockMined && !minerBlockCreator.isCancelled()) { try { blockMined = mineBlock(); } catch (final CancellationException ex) { @@ -84,6 +90,25 @@ public void run() { } } + /** + * Create a block with the given transactions and ommers. The list of transactions are validated + * as they are processed, and are not guaranteed to be included in the final block. If + * transactions must match exactly, the caller must verify they were all able to be included. + * + * @param parentHeader The header of the parent of the block to be produced + * @param transactions The list of transactions which may be included. + * @param ommers The list of ommers to include. + * @return the newly created block. + */ + public Block createBlock( + final BlockHeader parentHeader, + final List transactions, + final List ommers) { + final BlockCreator blockCreator = this.blockCreatorFactory.apply(parentHeader); + final long timestamp = scheduler.getNextTimestamp(parentHeader).getTimestampForHeader(); + return blockCreator.createBlock(transactions, ommers, timestamp); + } + protected boolean mineBlock() throws InterruptedException { // Ensure the block is allowed to be mined - i.e. the timestamp on the new block is sufficiently // ahead of the parent, and still within allowable clock tolerance. @@ -93,7 +118,7 @@ protected boolean mineBlock() throws InterruptedException { final Stopwatch stopwatch = Stopwatch.createStarted(); LOG.trace("Mining a new block with timestamp {}", newBlockTimestamp); - final Block block = blockCreator.createBlock(newBlockTimestamp); + final Block block = minerBlockCreator.createBlock(newBlockTimestamp); LOG.trace( "Block created, importing to local chain, block includes {} transactions", block.getBody().getTransactions().size()); @@ -123,7 +148,7 @@ protected boolean mineBlock() throws InterruptedException { } public void cancel() { - blockCreator.cancel(); + minerBlockCreator.cancel(); } private void notifyNewBlockListeners(final Block block) { diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java index b3755166a9..9e0318daf8 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java @@ -130,6 +130,17 @@ public TransactionSelectionResults buildTransactionListForBlock() { return transactionSelectionResult; } + /** + * Evaluate the given transactions and return the result of that evaluation. + * + * @param transactions The set of transactions to evaluate. + * @return The {@code TransactionSelectionResults} results of transaction evaluation. + */ + public TransactionSelectionResults evaluateTransactions(final List transactions) { + transactions.forEach(this::evaluateTransaction); + return transactionSelectionResult; + } + /* * Passed into the PendingTransactions, and is called on each transaction until sufficient * transactions are found which fill a block worth of gas. diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java index fdfea0e89e..dc3f1d824d 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java @@ -21,6 +21,7 @@ import tech.pegasys.pantheon.util.Subscribers; import java.util.Optional; +import java.util.function.Function; /** * Provides the EthHash specific aspects of the mining operation - i.e. getting the work definition, @@ -32,7 +33,7 @@ public class EthHashBlockMiner extends BlockMiner { public EthHashBlockMiner( - final EthHashBlockCreator blockCreator, + final Function blockCreator, final ProtocolSchedule protocolSchedule, final ProtocolContext protocolContext, final Subscribers observers, @@ -42,14 +43,14 @@ public EthHashBlockMiner( } public Optional getWorkDefinition() { - return blockCreator.getWorkDefinition(); + return minerBlockCreator.getWorkDefinition(); } public Optional getHashesPerSecond() { - return blockCreator.getHashesPerSecond(); + return minerBlockCreator.getHashesPerSecond(); } public boolean submitWork(final EthHashSolution solution) { - return blockCreator.submitWork(solution); + return minerBlockCreator.submitWork(solution); } } diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java index 70c470424a..6041890cf8 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java @@ -25,6 +25,7 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; +import java.util.function.Function; public class EthHashMinerExecutor extends AbstractMinerExecutor { @@ -53,33 +54,38 @@ public EthHashBlockMiner startAsyncMining( if (!coinbase.isPresent()) { throw new CoinbaseNotSetException("Unable to start mining without a coinbase."); } else { - final EthHashSolver solver = - new EthHashSolver(new RandomNonceGenerator(), new EthHasher.Light()); - final EthHashBlockCreator blockCreator = - new EthHashBlockCreator( - coinbase.get(), - parent -> extraData, - pendingTransactions, - protocolContext, - protocolSchedule, - (gasLimit) -> gasLimit, - solver, - minTransactionGasPrice, - parentHeader); - - final EthHashBlockMiner currentRunningMiner = - new EthHashBlockMiner( - blockCreator, - protocolSchedule, - protocolContext, - observers, - blockScheduler, - parentHeader); + final EthHashBlockMiner currentRunningMiner = createMiner(observers, parentHeader); executorService.execute(currentRunningMiner); return currentRunningMiner; } } + @Override + public EthHashBlockMiner createMiner(final BlockHeader parentHeader) { + return createMiner(Subscribers.none(), parentHeader); + } + + private EthHashBlockMiner createMiner( + final Subscribers observers, final BlockHeader parentHeader) { + final EthHashSolver solver = + new EthHashSolver(new RandomNonceGenerator(), new EthHasher.Light()); + final Function blockCreator = + (header) -> + new EthHashBlockCreator( + coinbase.get(), + parent -> extraData, + pendingTransactions, + protocolContext, + protocolSchedule, + (gasLimit) -> gasLimit, + solver, + minTransactionGasPrice, + parentHeader); + + return new EthHashBlockMiner( + blockCreator, protocolSchedule, protocolContext, observers, blockScheduler, parentHeader); + } + public void setCoinbase(final Address coinbase) { if (coinbase == null) { throw new IllegalArgumentException("Coinbase cannot be unset."); diff --git a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/MiningCoordinator.java b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/MiningCoordinator.java index 1a4e86dbec..e612fd2427 100644 --- a/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/MiningCoordinator.java +++ b/ethereum/blockcreation/src/main/java/tech/pegasys/pantheon/ethereum/blockcreation/MiningCoordinator.java @@ -13,11 +13,15 @@ package tech.pegasys.pantheon.ethereum.blockcreation; import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.ethereum.mainnet.EthHashSolution; import tech.pegasys.pantheon.ethereum.mainnet.EthHashSolverInputs; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.List; import java.util.Optional; public interface MiningCoordinator { @@ -52,4 +56,17 @@ default boolean submitWork(final EthHashSolution solution) { throw new UnsupportedOperationException( "Current consensus mechanism prevents submission of work solutions."); } + + /** + * Creates a block if possible, otherwise return an empty result + * + * @param parentHeader The parent block's header + * @param transactions The list of transactions to include + * @param ommers The list of ommers to include + * @return If supported, returns the block that was created, otherwise an empty response. + */ + Optional createBlock( + final BlockHeader parentHeader, + final List transactions, + final List ommers); } diff --git a/ethereum/blockcreation/src/test/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMinerTest.java b/ethereum/blockcreation/src/test/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMinerTest.java index c993655b18..a5794002cc 100644 --- a/ethereum/blockcreation/src/test/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMinerTest.java +++ b/ethereum/blockcreation/src/test/java/tech/pegasys/pantheon/ethereum/blockcreation/BlockMinerTest.java @@ -23,6 +23,7 @@ import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver; import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.BlockBody; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; import tech.pegasys.pantheon.ethereum.core.BlockImporter; import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; @@ -33,6 +34,7 @@ import java.math.BigInteger; import java.util.Optional; +import java.util.function.Function; import com.google.common.collect.Lists; import org.junit.Test; @@ -51,6 +53,8 @@ public void blockCreatedIsAddedToBlockChain() throws InterruptedException { final ProtocolContext protocolContext = new ProtocolContext<>(null, null, null); final EthHashBlockCreator blockCreator = mock(EthHashBlockCreator.class); + final Function blockCreatorSupplier = + (parentHeader) -> blockCreator; when(blockCreator.createBlock(anyLong())).thenReturn(blockToCreate); final BlockImporter blockImporter = mock(BlockImporter.class); @@ -66,7 +70,7 @@ public void blockCreatedIsAddedToBlockChain() throws InterruptedException { when(scheduler.waitUntilNextBlockCanBeMined(any())).thenReturn(5L); final BlockMiner miner = new EthHashBlockMiner( - blockCreator, + blockCreatorSupplier, protocolSchedule, protocolContext, subscribersContaining(observer), @@ -90,6 +94,8 @@ public void failureToImportDoesNotTriggerObservers() throws InterruptedException final ProtocolContext protocolContext = new ProtocolContext<>(null, null, null); final EthHashBlockCreator blockCreator = mock(EthHashBlockCreator.class); + final Function blockCreatorSupplier = + (parentHeader) -> blockCreator; when(blockCreator.createBlock(anyLong())).thenReturn(blockToCreate); final BlockImporter blockImporter = mock(BlockImporter.class); @@ -104,7 +110,7 @@ public void failureToImportDoesNotTriggerObservers() throws InterruptedException when(scheduler.waitUntilNextBlockCanBeMined(any())).thenReturn(5L); final BlockMiner miner = new EthHashBlockMiner( - blockCreator, + blockCreatorSupplier, protocolSchedule, protocolContext, subscribersContaining(observer), diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Address.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Address.java index 5ee21a05b2..5e2d476122 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Address.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Address.java @@ -14,6 +14,7 @@ import static com.google.common.base.Preconditions.checkArgument; +import tech.pegasys.pantheon.crypto.SECP256K1.PublicKey; import tech.pegasys.pantheon.ethereum.rlp.RLP; import tech.pegasys.pantheon.ethereum.rlp.RLPException; import tech.pegasys.pantheon.ethereum.rlp.RLPInput; @@ -82,6 +83,10 @@ public static Address extract(final Hash hash) { return wrap(hash.slice(12, 20)); } + public static Address extract(final PublicKey publicKey) { + return Address.extract(Hash.hash(publicKey.getEncodedBytes())); + } + /** * Parse an hexadecimal string representing an account address. * diff --git a/pantheon/build.gradle b/pantheon/build.gradle index e12c5640e3..a20ebc400d 100644 --- a/pantheon/build.gradle +++ b/pantheon/build.gradle @@ -46,6 +46,8 @@ dependencies { implementation project(':nat') implementation project(':services:kvstore') + implementation 'com.fasterxml.jackson.core:jackson-databind' + compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: '2.9.8' implementation 'com.graphql-java:graphql-java' implementation 'com.google.guava:guava' implementation 'info.picocli:picocli' diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java b/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java index ce8f09bb56..638de717c2 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java @@ -14,6 +14,7 @@ import static org.apache.logging.log4j.LogManager.getLogger; +import tech.pegasys.pantheon.chainimport.ChainImporter; import tech.pegasys.pantheon.cli.PantheonCommand; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.services.PantheonPluginContextImpl; @@ -37,6 +38,7 @@ public static void main(final String... args) { logger, new BlockImporter(), new BlockExporter(), + ChainImporter::new, new RunnerBuilder(), new PantheonController.Builder(), new PantheonPluginContextImpl(), diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/BlockData.java b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/BlockData.java new file mode 100644 index 0000000000..06b20562d3 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/BlockData.java @@ -0,0 +1,90 @@ +/* + * 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.chainimport; + +import tech.pegasys.pantheon.chainimport.TransactionData.NonceProvider; +import tech.pegasys.pantheon.ethereum.core.Account; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BlockData { + + private final Optional number; + private final Optional parentHash; + private final List transactionData; + private final Optional
coinbase; + private final Optional extraData; + + @JsonCreator + public BlockData( + @JsonProperty("number") final Optional number, + @JsonProperty("parentHash") final Optional parentHash, + @JsonProperty("coinbase") final Optional coinbase, + @JsonProperty("extraData") final Optional extraData, + @JsonProperty("transactions") final List transactions) { + this.number = number.map(UInt256::fromHexString).map(UInt256::toLong); + this.parentHash = parentHash.map(Bytes32::fromHexString).map(Hash::wrap); + this.coinbase = coinbase.map(Address::fromHexString); + this.extraData = extraData.map(BytesValue::fromHexStringLenient); + this.transactionData = transactions; + } + + public Optional getNumber() { + return number; + } + + public Optional getParentHash() { + return parentHash; + } + + public Optional
getCoinbase() { + return coinbase; + } + + public Optional getExtraData() { + return extraData; + } + + public Stream streamTransactions(final WorldState worldState) { + final NonceProvider nonceProvider = getNonceProvider(worldState); + return transactionData.stream().map((tx) -> tx.getSignedTransaction(nonceProvider)); + } + + public NonceProvider getNonceProvider(final WorldState worldState) { + final HashMap currentNonceValues = new HashMap<>(); + return (Address address) -> + currentNonceValues.compute( + address, + (addr, currentValue) -> { + if (currentValue == null) { + return Optional.ofNullable(worldState.get(address)) + .map(Account::getNonce) + .orElse(0L); + } + return currentValue + 1; + }); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainData.java b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainData.java new file mode 100644 index 0000000000..7c3127fadc --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainData.java @@ -0,0 +1,32 @@ +/* + * 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.chainimport; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ChainData { + + private final List blocks; + + @JsonCreator + public ChainData(@JsonProperty("blocks") final List blocks) { + this.blocks = blocks; + } + + public List getBlocks() { + return blocks; + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainImporter.java b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainImporter.java new file mode 100644 index 0000000000..6f1088f0cb --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/ChainImporter.java @@ -0,0 +1,248 @@ +/* + * 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.chainimport; + +import tech.pegasys.pantheon.config.GenesisConfigOptions; +import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.ethereum.blockcreation.MiningCoordinator; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockImporter; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +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.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ChainImporter { + private static final Logger LOG = LogManager.getLogger(); + + private final ObjectMapper mapper; + private final PantheonController controller; + + public ChainImporter(final PantheonController 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); + } + + public void importChain(final String chainJson) throws IOException { + warnIfDatabaseIsNotEmpty(); + + final ChainData chainData = mapper.readValue(chainJson, ChainData.class); + + final List 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()) + .get(); + final List 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 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.isEthHash()) { + // For simplicity only set these for ethash. Other consensus algorithms use these fields for + // special purposes or ignore them + miner.setCoinbase(blockData.getCoinbase().orElse(Address.ZERO)); + miner.setExtraData(blockData.getExtraData().orElse(BytesValue.EMPTY)); + } else if (blockData.getCoinbase().isPresent() || blockData.getExtraData().isPresent()) { + // Fail if these fields are set for non-ethash chains + final Stream.Builder 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() + .getByBlockNumber(block.getHeader().getNumber()) + .getBlockImporter(); + + final boolean imported = + importer.importBlock(controller.getProtocolContext(), block, HeaderValidationMode.NONE); + if (imported) { + 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 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 importedBlocks) { + final List 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 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; + Optional maybeHeader = + importedBlocks.stream() + .map(Block::getHeader) + .filter(h -> h.getNumber() == targetParentBlockNumber) + .findFirst(); + if (maybeHeader.isPresent()) { + return maybeHeader.get(); + } + } + + 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)); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/TransactionData.java b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/TransactionData.java new file mode 100644 index 0000000000..fb133ea481 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/chainimport/TransactionData.java @@ -0,0 +1,73 @@ +/* + * 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.chainimport; + +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.crypto.SECP256K1.PrivateKey; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TransactionData { + + private final long gasLimit; + private final Wei gasPrice; + private final BytesValue data; + private final Wei value; + private final Optional
to; + private final PrivateKey privateKey; + + @JsonCreator + public TransactionData( + @JsonProperty("gasLimit") final String gasLimit, + @JsonProperty("gasPrice") final String gasPrice, + @JsonProperty("data") final Optional data, + @JsonProperty("value") final Optional value, + @JsonProperty("to") final Optional to, + @JsonProperty("fromPrivateKey") final String fromPrivateKey) { + this.gasLimit = UInt256.fromHexString(gasLimit).toLong(); + this.gasPrice = Wei.fromHexString(gasPrice); + this.data = data.map(BytesValue::fromHexString).orElse(BytesValue.EMPTY); + this.value = value.map(Wei::fromHexString).orElse(Wei.ZERO); + this.to = to.map(Address::fromHexString); + this.privateKey = PrivateKey.create(Bytes32.fromHexString(fromPrivateKey)); + } + + public Transaction getSignedTransaction(final NonceProvider nonceProvider) { + KeyPair keyPair = KeyPair.create(privateKey); + + final Address fromAddress = Address.extract(keyPair.getPublicKey()); + final long nonce = nonceProvider.get(fromAddress); + return Transaction.builder() + .gasLimit(gasLimit) + .gasPrice(gasPrice) + .nonce(nonce) + .payload(data) + .value(value) + .to(to.orElse(null)) + .signAndBuild(keyPair); + } + + @FunctionalInterface + public interface NonceProvider { + long get(final Address address); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 285f56a0c6..9d63456bd2 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -51,6 +51,7 @@ import tech.pegasys.pantheon.cli.subcommands.PublicKeySubCommand.KeyLoader; import tech.pegasys.pantheon.cli.subcommands.RetestethSubCommand; import tech.pegasys.pantheon.cli.subcommands.blocks.BlocksSubCommand; +import tech.pegasys.pantheon.cli.subcommands.blocks.BlocksSubCommand.ChainImporterFactory; import tech.pegasys.pantheon.cli.subcommands.operator.OperatorSubCommand; import tech.pegasys.pantheon.cli.subcommands.rlp.RLPSubCommand; import tech.pegasys.pantheon.cli.util.ConfigOptionSearchAndRunHandler; @@ -58,6 +59,7 @@ import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.controller.KeyPairUtil; import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.controller.PantheonControllerBuilder; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.MiningParameters; import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; @@ -93,7 +95,6 @@ import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration; import tech.pegasys.pantheon.util.BlockExporter; import tech.pegasys.pantheon.util.BlockImporter; -import tech.pegasys.pantheon.util.InvalidConfigurationException; import tech.pegasys.pantheon.util.PermissioningConfigurationValidator; import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.number.Fraction; @@ -152,6 +153,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { private final Logger logger; + private final ChainImporterFactory chainImporterFactory; private CommandLine commandLine; @@ -633,6 +635,7 @@ public PantheonCommand( final Logger logger, final BlockImporter blockImporter, final BlockExporter blockExporter, + final ChainImporterFactory chainImporterFactory, final RunnerBuilder runnerBuilder, final PantheonController.Builder controllerBuilderFactory, final PantheonPluginContextImpl pantheonPluginContext, @@ -640,6 +643,7 @@ public PantheonCommand( this.logger = logger; this.blockImporter = blockImporter; this.blockExporter = blockExporter; + this.chainImporterFactory = chainImporterFactory; this.runnerBuilder = runnerBuilder; this.controllerBuilderFactory = controllerBuilderFactory; this.pantheonPluginContext = pantheonPluginContext; @@ -685,7 +689,8 @@ private PantheonCommand addSubCommands( final AbstractParseResultHandler> resultHandler, final InputStream in) { commandLine.addSubcommand( BlocksSubCommand.COMMAND_NAME, - new BlocksSubCommand(blockImporter, blockExporter, resultHandler.out())); + new BlocksSubCommand( + blockImporter, blockExporter, chainImporterFactory, resultHandler.out())); commandLine.addSubcommand( PublicKeySubCommand.COMMAND_NAME, new PublicKeySubCommand(resultHandler.out(), getKeyLoader())); @@ -873,6 +878,16 @@ private PantheonCommand controller() { } public PantheonController buildController() { + try { + return getControllerBuilder().build(); + } catch (final IOException e) { + throw new ExecutionException(this.commandLine, "Invalid path", e); + } catch (final Exception e) { + throw new ExecutionException(this.commandLine, e.getMessage(), e); + } + } + + public PantheonControllerBuilder getControllerBuilder() { try { return controllerBuilderFactory .fromEthNetworkConfig(updateNetworkConfig(getNetwork())) @@ -887,11 +902,8 @@ public PantheonController buildController() { .metricsSystem(metricsSystem.get()) .privacyParameters(privacyParameters()) .clock(Clock.systemUTC()) - .isRevertReasonEnabled(isRevertReasonEnabled) - .build(); - } catch (final InvalidConfigurationException e) { - throw new ExecutionException(this.commandLine, e.getMessage()); - } catch (final IOException e) { + .isRevertReasonEnabled(isRevertReasonEnabled); + } catch (IOException e) { throw new ExecutionException(this.commandLine, "Invalid path", e); } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlockFormat.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlockFormat.java new file mode 100644 index 0000000000..e7207986c9 --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlockFormat.java @@ -0,0 +1,18 @@ +/* + * 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.cli.subcommands.blocks; + +public enum BlockFormat { + RLP, + JSON +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommand.java index 51b1461e2c..13d48baac3 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommand.java @@ -18,14 +18,20 @@ import static tech.pegasys.pantheon.cli.DefaultCommandValues.MANDATORY_LONG_FORMAT_HELP; import static tech.pegasys.pantheon.cli.subcommands.blocks.BlocksSubCommand.COMMAND_NAME; +import tech.pegasys.pantheon.chainimport.ChainImporter; import tech.pegasys.pantheon.cli.PantheonCommand; import tech.pegasys.pantheon.cli.subcommands.blocks.BlocksSubCommand.ExportSubCommand; import tech.pegasys.pantheon.cli.subcommands.blocks.BlocksSubCommand.ImportSubCommand; +import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.MiningParameters; +import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.metrics.prometheus.MetricsService; import tech.pegasys.pantheon.util.BlockExporter; import tech.pegasys.pantheon.util.BlockImporter; +import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.BufferedWriter; import java.io.File; @@ -45,6 +51,7 @@ import picocli.CommandLine.ExecutionException; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; import picocli.CommandLine.ParentCommand; import picocli.CommandLine.Spec; @@ -70,13 +77,18 @@ public class BlocksSubCommand implements Runnable { private final BlockImporter blockImporter; private final BlockExporter blockExporter; + private final ChainImporterFactory chainImporterFactory; private final PrintStream out; public BlocksSubCommand( - final BlockImporter blockImporter, final BlockExporter blockExporter, final PrintStream out) { + final BlockImporter blockImporter, + final BlockExporter blockExporter, + final ChainImporterFactory chainImporterFactory, + final PrintStream out) { this.blockImporter = blockImporter; this.blockExporter = blockExporter; + this.chainImporterFactory = chainImporterFactory; this.out = out; } @@ -103,13 +115,25 @@ static class ImportSubCommand implements Runnable { names = "--from", required = true, paramLabel = MANDATORY_FILE_FORMAT_HELP, - description = "File containing blocks to import", + description = "File containing blocks to import.", arity = "1..1") private final File blocksImportFile = null; + @Option( + names = "--format", + hidden = true, + description = + "The type of data to be imported, possible values are: ${COMPLETION-CANDIDATES} (default: ${DEFAULT-VALUE}).", + arity = "1..1") + private final BlockFormat format = BlockFormat.RLP; + + @SuppressWarnings("unused") + @Spec + private CommandSpec spec; + @Override public void run() { - LOG.info("Runs import sub command with blocksImportFile : {}", blocksImportFile); + LOG.info("Import {} block data from {}", format, blocksImportFile); checkCommand(parentCommand); checkNotNull(parentCommand.blockImporter); @@ -118,19 +142,27 @@ public void run() { try { // As blocksImportFile even if initialized as null is injected by PicoCLI and param is - // mandatory - // So we are sure it's always not null, we can remove the warning + // mandatory. So we are sure it's always not null, we can remove the warning. //noinspection ConstantConditions final Path path = blocksImportFile.toPath(); - - parentCommand.blockImporter.importBlockchain( - path, parentCommand.parentCommand.buildController()); + final PantheonController controller = createController(); + switch (format) { + case RLP: + importRlpBlocks(controller, path); + break; + case JSON: + importJsonBlocks(controller, path); + break; + default: + throw new ParameterException( + spec.commandLine(), "Unsupported format: " + format.toString()); + } } catch (final FileNotFoundException e) { throw new ExecutionException( - new CommandLine(this), "Could not find file to import: " + blocksImportFile); + spec.commandLine(), "Could not find file to import: " + blocksImportFile); } catch (final IOException e) { throw new ExecutionException( - new CommandLine(this), "Unable to import blocks from " + blocksImportFile, e); + spec.commandLine(), "Unable to import blocks from " + blocksImportFile, e); } finally { metricsService.ifPresent(MetricsService::stop); } @@ -140,6 +172,42 @@ private static void checkCommand(final BlocksSubCommand parentCommand) { checkNotNull(parentCommand); checkNotNull(parentCommand.parentCommand); } + + private PantheonController createController() { + try { + // Set some defaults + return parentCommand + .parentCommand + .getControllerBuilder() + .miningParameters(getMiningParameters()) + .build(); + } catch (final IOException e) { + throw new ExecutionException(new CommandLine(parentCommand), "Invalid path", e); + } catch (final Exception e) { + throw new ExecutionException(new CommandLine(parentCommand), e.getMessage(), e); + } + } + + private MiningParameters getMiningParameters() { + final Wei minTransactionGasPrice = Wei.ZERO; + // Extradata and coinbase can be configured on a per-block level via the json file + final Address coinbase = Address.ZERO; + final BytesValue extraData = BytesValue.EMPTY; + return new MiningParameters(coinbase, minTransactionGasPrice, extraData, false); + } + + private void importJsonBlocks(final PantheonController controller, final Path path) + throws IOException { + + ChainImporter importer = parentCommand.chainImporterFactory.get(controller); + final String jsonData = Files.readString(path); + importer.importChain(jsonData); + } + + private void importRlpBlocks(final PantheonController controller, final Path path) + throws IOException { + parentCommand.blockImporter.importBlockchain(path, controller); + } } /** @@ -256,4 +324,9 @@ private static Optional initMetrics(final BlocksSubCommand paren } return metricsService; } + + @FunctionalInterface + public interface ChainImporterFactory { + ChainImporter get(PantheonController controller); + } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/management/RestfulRouteBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/management/RestfulRouteBuilder.java deleted file mode 100644 index 240f9285ef..0000000000 --- a/pantheon/src/main/java/tech/pegasys/pantheon/management/RestfulRouteBuilder.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2018 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.management; - -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Handler; -import io.vertx.core.http.HttpMethod; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; - -class RestfulRouteBuilder { - - private RestfulRouteBuilder() {} - - static RestfulRoute restfulRoute(final Router router, final String path) { - return new RestfulRoute(router, path); - } - - static class RestfulRoute { - private final Router router; - private final String path; - private final Map> methods; - - private RestfulRoute(final Router router, final String path) { - this.router = router; - this.path = path; - this.methods = Collections.emptyMap(); - } - - private RestfulRoute( - final Router router, - final String path, - final Map> methods) { - this.router = router; - this.path = path; - this.methods = methods; - } - - RestfulRoute method(final HttpMethod method, final Consumer routeBuilder) { - if (method == HttpMethod.HEAD) { - throw new IllegalArgumentException("HEAD method should not be handled explicitly"); - } - if (method == HttpMethod.OPTIONS) { - throw new IllegalArgumentException("OPTIONS method should not be handled explicitly"); - } - - final Map> updatedMethods = - new HashMap<>(this.methods); - updatedMethods.put(method, routeBuilder); - return new RestfulRoute(router, path, updatedMethods); - } - - void build() { - methods.forEach( - (method, routeBuilder) -> { - Route route = router.route(path); - route = route.method(method); - if (method == HttpMethod.GET) { - route = route.method(HttpMethod.HEAD); - } - routeBuilder.accept(new RestfulMethodRoute(route)); - }); - createUnmatchedContentTypeRoute(); - createOptionsRoute(); - createUnmatchedMethodRoute(); - } - - private void createUnmatchedContentTypeRoute() { - final Route route = router.route(path); - methods.keySet().forEach(route::method); - route.handler( - routingContext -> - routingContext - .response() - .setStatusCode(HttpResponseStatus.NOT_ACCEPTABLE.code()) - .end()); - } - - private void createOptionsRoute() { - final Set visibleMethods = new HashSet<>(this.methods.keySet()); - visibleMethods.add(HttpMethod.OPTIONS); - if (visibleMethods.contains(HttpMethod.GET)) { - visibleMethods.add(HttpMethod.HEAD); - } - - final String methodsStrings = - String.join( - ",", - visibleMethods.stream().map(HttpMethod::name).sorted().collect(Collectors.toList())); - router - .route(path) - .method(HttpMethod.OPTIONS) - .handler( - routingContext -> - routingContext - .response() - .setStatusCode(HttpResponseStatus.NO_CONTENT.code()) - .putHeader(HttpHeaderNames.ALLOW.toString(), methodsStrings) - .end()); - } - - private void createUnmatchedMethodRoute() { - router - .route(path) - .handler( - routingContext -> - routingContext - .response() - .setStatusCode(HttpResponseStatus.METHOD_NOT_ALLOWED.code()) - .end()); - } - } - - static class RestfulMethodRoute { - private final Route route; - - RestfulMethodRoute(final Route route) { - this.route = route; - } - - RestfulMethodRoute produces(final String... contentTypes) { - for (final String contentType : contentTypes) { - route.produces(contentType); - } - return this; - } - - void handler(final Handler handler) { - route.handler(handler); - } - } -} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/chainimport/ChainImporterTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/chainimport/ChainImporterTest.java new file mode 100644 index 0000000000..595dd20302 --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/chainimport/ChainImporterTest.java @@ -0,0 +1,429 @@ +/* + * 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.chainimport; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import tech.pegasys.pantheon.config.GenesisConfigFile; +import tech.pegasys.pantheon.config.JsonUtil; +import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.chain.Blockchain; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockBody; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider; +import tech.pegasys.pantheon.ethereum.core.MiningParametersTestBuilder; +import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.ethereum.eth.EthProtocolConfiguration; +import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPoolConfiguration; +import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; +import tech.pegasys.pantheon.testutil.TestClock; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.io.Resources; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +public abstract class ChainImporterTest { + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + protected final String consensusEngine; + protected final GenesisConfigFile genesisConfigFile; + protected final boolean isEthash; + + public ChainImporterTest(final String consensusEngine) throws IOException { + this.consensusEngine = consensusEngine; + final String genesisData = getFileContents("genesis.json"); + this.genesisConfigFile = GenesisConfigFile.fromConfig(genesisData); + this.isEthash = genesisConfigFile.getConfigOptions().isEthHash(); + } + + public static class SingletonTests extends ChainImporterTest { + public SingletonTests() throws IOException { + super("unsupported"); + } + + @Test + public void importChain_unsupportedConsensusAlgorithm() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + final String jsonData = getFileContents("clique", "blocks-import-valid.json"); + + assertThatThrownBy(() -> importer.importChain(jsonData)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Unable to create block using current consensus engine: " + + genesisConfigFile.getConfigOptions().getConsensusEngine()); + } + } + + @RunWith(Parameterized.class) + public static class ParameterizedTests extends ChainImporterTest { + + public ParameterizedTests(final String consensusEngine) throws IOException { + super(consensusEngine); + } + + @Parameters(name = "Name: {0}") + public static Collection getParameters() { + Object[][] params = {{"ethash"}, {"clique"}}; + return Arrays.asList(params); + } + + @Test + public void importChain_validJson_withBlockNumbers() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + final String jsonData = getFileContents("blocks-import-valid.json"); + importer.importChain(jsonData); + + final Blockchain blockchain = controller.getProtocolContext().getBlockchain(); + + // Check blocks were imported + assertThat(blockchain.getChainHead().getHeight()).isEqualTo(4); + // Get imported blocks + List blocks = new ArrayList<>(4); + for (int i = 0; i < 4; i++) { + blocks.add(getBlockAt(blockchain, i + 1)); + } + + // Check block 1 + Block block = blocks.get(0); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(block.getHeader().getCoinbase()).isEqualTo(Address.ZERO); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(2); + // Check first tx + Transaction tx = block.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("fe3b557e8fb62b89f4916b721be55ceb828dbd73")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFF1L); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(1L)); + assertThat(tx.getNonce()).isEqualTo(0L); + // Check second tx + tx = block.getBody().getTransactions().get(1); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("fe3b557e8fb62b89f4916b721be55ceb828dbd73")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("f17f52151EbEF6C7334FAD080c5704D77216b732")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFF2L); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xEF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(0L)); + assertThat(tx.getNonce()).isEqualTo(1L); + + // Check block 2 + block = blocks.get(1); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.fromHexString("0x1234")); + assertThat(block.getHeader().getCoinbase()).isEqualTo(Address.fromHexString("0x02")); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(1); + // Check first tx + tx = block.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("f17f52151EbEF6C7334FAD080c5704D77216b732")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFFFL); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(0L)); + assertThat(tx.getNonce()).isEqualTo(0L); + + // Check block 3 + block = blocks.get(2); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.fromHexString("0x3456")); + assertThat(block.getHeader().getCoinbase()) + .isEqualTo(Address.fromHexString("f17f52151EbEF6C7334FAD080c5704D77216b732")); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(0); + + // Check block 4 + block = blocks.get(3); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(block.getHeader().getCoinbase()).isEqualTo(Address.ZERO); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(1); + // Check first tx + tx = block.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getTo()).isEmpty(); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFFFL); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(0L)); + assertThat(tx.getNonce()).isEqualTo(1L); + } + + @Test + public void importChain_validJson_noBlockIdentifiers() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + final String jsonData = getFileContents("blocks-import-valid-no-block-identifiers.json"); + importer.importChain(jsonData); + + final Blockchain blockchain = controller.getProtocolContext().getBlockchain(); + + // Check blocks were imported + assertThat(blockchain.getChainHead().getHeight()).isEqualTo(4); + // Get imported blocks + List blocks = new ArrayList<>(4); + for (int i = 0; i < 4; i++) { + blocks.add(getBlockAt(blockchain, i + 1)); + } + + // Check block 1 + Block block = blocks.get(0); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(block.getHeader().getCoinbase()).isEqualTo(Address.ZERO); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(2); + // Check first tx + Transaction tx = block.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("fe3b557e8fb62b89f4916b721be55ceb828dbd73")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFF1L); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(1L)); + assertThat(tx.getNonce()).isEqualTo(0L); + // Check second tx + tx = block.getBody().getTransactions().get(1); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("fe3b557e8fb62b89f4916b721be55ceb828dbd73")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("f17f52151EbEF6C7334FAD080c5704D77216b732")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFF2L); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xEF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(0L)); + assertThat(tx.getNonce()).isEqualTo(1L); + + // Check block 2 + block = blocks.get(1); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.fromHexString("0x1234")); + assertThat(block.getHeader().getCoinbase()).isEqualTo(Address.fromHexString("0x02")); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(1); + // Check first tx + tx = block.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("f17f52151EbEF6C7334FAD080c5704D77216b732")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFFFL); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(0L)); + assertThat(tx.getNonce()).isEqualTo(0L); + + // Check block 3 + block = blocks.get(2); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.fromHexString("0x3456")); + assertThat(block.getHeader().getCoinbase()) + .isEqualTo(Address.fromHexString("f17f52151EbEF6C7334FAD080c5704D77216b732")); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(0); + + // Check block 4 + block = blocks.get(3); + if (isEthash) { + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(block.getHeader().getCoinbase()).isEqualTo(Address.ZERO); + } + assertThat(block.getBody().getTransactions().size()).isEqualTo(1); + // Check first tx + tx = block.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getTo()).isEmpty(); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFFFL); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(0L)); + assertThat(tx.getNonce()).isEqualTo(1L); + } + + @Test + public void importChain_validJson_withParentHashes() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + String jsonData = getFileContents("blocks-import-valid.json"); + + importer.importChain(jsonData); + + final Blockchain blockchain = controller.getProtocolContext().getBlockchain(); + + // Check blocks were imported + assertThat(blockchain.getChainHead().getHeight()).isEqualTo(4); + // Get imported blocks + List blocks = new ArrayList<>(4); + for (int i = 0; i < 4; i++) { + blocks.add(getBlockAt(blockchain, i + 1)); + } + + // Run new import based on first file + jsonData = getFileContents("blocks-import-valid-addendum.json"); + ObjectNode newImportData = JsonUtil.objectNodeFromString(jsonData); + final ObjectNode block0 = (ObjectNode) newImportData.get("blocks").get(0); + final Block parentBlock = blocks.get(3); + block0.put("parentHash", parentBlock.getHash().toString()); + final String newImportJsonData = JsonUtil.getJson(newImportData); + importer.importChain(newImportJsonData); + + // Check blocks were imported + assertThat(blockchain.getChainHead().getHeight()).isEqualTo(5); + final Block newBlock = getBlockAt(blockchain, parentBlock.getHeader().getNumber() + 1L); + + // Check block 1 + assertThat(newBlock.getHeader().getParentHash()).isEqualTo(parentBlock.getHash()); + if (isEthash) { + assertThat(newBlock.getHeader().getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(newBlock.getHeader().getCoinbase()).isEqualTo(Address.ZERO); + } + assertThat(newBlock.getBody().getTransactions().size()).isEqualTo(1); + // Check first tx + Transaction tx = newBlock.getBody().getTransactions().get(0); + assertThat(tx.getSender()) + .isEqualTo(Address.fromHexString("fe3b557e8fb62b89f4916b721be55ceb828dbd73")); + assertThat(tx.getTo()) + .hasValue(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + assertThat(tx.getGasLimit()).isEqualTo(0xFFFFF1L); + assertThat(tx.getGasPrice()).isEqualTo(Wei.fromHexString("0xFF")); + assertThat(tx.getValue()).isEqualTo(Wei.of(1L)); + assertThat(tx.getNonce()).isEqualTo(2L); + } + + @Test + public void importChain_invalidParent() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + final String jsonData = getFileContents("blocks-import-invalid-bad-parent.json"); + + assertThatThrownBy(() -> importer.importChain(jsonData)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Unable to locate block parent at 2456"); + } + + @Test + public void importChain_invalidTransaction() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + final String jsonData = getFileContents("blocks-import-invalid-bad-tx.json"); + + assertThatThrownBy(() -> importer.importChain(jsonData)) + .isInstanceOf(IllegalStateException.class) + .hasMessageStartingWith( + "Unable to create block. 1 transaction(s) were found to be invalid."); + } + + @Test + public void importChain_specialFields() throws IOException { + final PantheonController controller = createController(); + final ChainImporter importer = new ChainImporter<>(controller); + + final String jsonData = getFileContents("blocks-import-special-fields.json"); + + if (isEthash) { + importer.importChain(jsonData); + final Blockchain blockchain = controller.getProtocolContext().getBlockchain(); + final Block block = getBlockAt(blockchain, 1); + assertThat(block.getHeader().getExtraData()).isEqualTo(BytesValue.fromHexString("0x0123")); + assertThat(block.getHeader().getCoinbase()) + .isEqualTo(Address.fromHexString("627306090abaB3A6e1400e9345bC60c78a8BEf57")); + } else { + assertThatThrownBy(() -> importer.importChain(jsonData)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Some fields (coinbase, extraData) are unsupported by the current consensus engine: " + + genesisConfigFile.getConfigOptions().getConsensusEngine()); + } + } + } + + protected Block getBlockAt(final Blockchain blockchain, final long blockNumber) { + final BlockHeader header = blockchain.getBlockHeader(blockNumber).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + return new Block(header, body); + } + + protected String getFileContents(final String filename) throws IOException { + return getFileContents(consensusEngine, filename); + } + + protected String getFileContents(final String folder, final String filename) throws IOException { + final String filePath = folder + "/" + filename; + final URL fileURL = this.getClass().getResource(filePath); + return Resources.toString(fileURL, UTF_8); + } + + protected PantheonController createController() throws IOException { + return createController(genesisConfigFile); + } + + protected PantheonController createController(final GenesisConfigFile genesisConfigFile) + throws IOException { + final Path dataDir = folder.newFolder().toPath(); + return new PantheonController.Builder() + .fromGenesisConfig(genesisConfigFile) + .synchronizerConfiguration(SynchronizerConfiguration.builder().build()) + .ethProtocolConfiguration(EthProtocolConfiguration.defaultConfig()) + .storageProvider(new InMemoryStorageProvider()) + .networkId(10) + .miningParameters( + new MiningParametersTestBuilder() + .minTransactionGasPrice(Wei.ZERO) + .enabled(false) + .build()) + .nodeKeys(KeyPair.generate()) + .metricsSystem(new NoOpMetricsSystem()) + .privacyParameters(PrivacyParameters.DEFAULT) + .dataDirectory(dataDir) + .clock(TestClock.fixed()) + .transactionPoolConfiguration(TransactionPoolConfiguration.builder().build()) + .build(); + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index b320adf355..39b0edc6d7 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -23,6 +23,7 @@ import tech.pegasys.pantheon.Runner; import tech.pegasys.pantheon.RunnerBuilder; +import tech.pegasys.pantheon.chainimport.ChainImporter; import tech.pegasys.pantheon.cli.config.EthNetworkConfig; import tech.pegasys.pantheon.cli.options.EthProtocolOptions; import tech.pegasys.pantheon.cli.options.MetricsCLIOptions; @@ -31,6 +32,7 @@ import tech.pegasys.pantheon.cli.options.SynchronizerOptions; import tech.pegasys.pantheon.cli.options.TransactionPoolOptions; import tech.pegasys.pantheon.cli.subcommands.PublicKeySubCommand.KeyLoader; +import tech.pegasys.pantheon.cli.subcommands.blocks.BlocksSubCommand.ChainImporterFactory; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.controller.PantheonControllerBuilder; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; @@ -101,6 +103,7 @@ public abstract class CommandTestAbstract { @Mock protected PantheonController mockController; @Mock protected BlockImporter mockBlockImporter; @Mock protected BlockExporter mockBlockExporter; + @Mock protected ChainImporter chainImporter; @Mock protected Logger mockLogger; @Mock protected PantheonPluginContextImpl mockPantheonPluginContext; @@ -211,6 +214,11 @@ protected TestPantheonCommand parseCommand(final InputStream in, final String... return parseCommand(f -> KeyPair.generate(), in, args); } + @SuppressWarnings("unchecked") + private ChainImporter chainImporterFactory(final PantheonController controller) { + return (ChainImporter) chainImporter; + } + private TestPantheonCommand parseCommand( final KeyLoader keyLoader, final InputStream in, final String... args) { // turn off ansi usage globally in picocli @@ -221,6 +229,7 @@ private TestPantheonCommand parseCommand( mockLogger, mockBlockImporter, mockBlockExporter, + this::chainImporterFactory, mockRunnerBuilder, mockControllerBuilderFactory, keyLoader, @@ -250,6 +259,7 @@ protected KeyLoader getKeyLoader() { final Logger mockLogger, final BlockImporter mockBlockImporter, final BlockExporter mockBlockExporter, + final ChainImporterFactory chainImporterFactory, final RunnerBuilder mockRunnerBuilder, final PantheonController.Builder controllerBuilderFactory, final KeyLoader keyLoader, @@ -259,6 +269,7 @@ protected KeyLoader getKeyLoader() { mockLogger, mockBlockImporter, mockBlockExporter, + chainImporterFactory, mockRunnerBuilder, controllerBuilderFactory, pantheonPluginContext, diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/BlockSubCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommandTest.java similarity index 84% rename from pantheon/src/test/java/tech/pegasys/pantheon/cli/BlockSubCommandTest.java rename to pantheon/src/test/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommandTest.java index 07697c691a..2c993bc109 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/BlockSubCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/subcommands/blocks/BlocksSubCommandTest.java @@ -10,15 +10,18 @@ * 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.cli; +package tech.pegasys.pantheon.cli.subcommands.blocks; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; import static org.assertj.core.util.Arrays.asList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import tech.pegasys.pantheon.cli.CommandTestAbstract; import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.controller.PantheonController; import tech.pegasys.pantheon.crypto.SECP256K1; @@ -38,6 +41,8 @@ import java.io.File; import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; import java.nio.file.Path; import org.junit.Rule; @@ -45,7 +50,7 @@ import org.junit.rules.TemporaryFolder; import picocli.CommandLine.Model.CommandSpec; -public class BlockSubCommandTest extends CommandTestAbstract { +public class BlocksSubCommandTest extends CommandTestAbstract { @Rule public final TemporaryFolder folder = new TemporaryFolder(); @@ -65,10 +70,16 @@ public class BlockSubCommandTest extends CommandTestAbstract { private static final String EXPECTED_BLOCK_IMPORT_USAGE = "Usage: pantheon blocks import [-hV] --from=" + // "Usage: pantheon blocks import [-hV] [--format=] --from=" + System.lineSeparator() + "This command imports blocks from a file into the database." + // Hide format for while JSON option is under development + // + System.lineSeparator() + // + " --format= The type of data to be imported, possible values + // are: RLP,\n" + // + " JSON (default: RLP)." + System.lineSeparator() - + " --from= File containing blocks to import" + + " --from= File containing blocks to import." + System.lineSeparator() + " -h, --help Show this help message and exit." + System.lineSeparator() @@ -140,7 +151,7 @@ public void callingBlockImportSubCommandWithoutPathMustDisplayErrorAndUsage() { @Test public void callingBlockImportSubCommandHelpMustDisplayUsage() { parseCommand(BLOCK_SUBCOMMAND_NAME, BLOCK_IMPORT_SUBCOMMAND_NAME, "--help"); - assertThat(commandOutput.toString()).startsWith(EXPECTED_BLOCK_IMPORT_USAGE); + assertThat(commandOutput.toString()).isEqualToIgnoringWhitespace(EXPECTED_BLOCK_IMPORT_USAGE); assertThat(commandErrorOutput.toString()).isEmpty(); } @@ -158,6 +169,48 @@ public void callingBlockImportSubCommandWithPathMustImportBlocksWithThisPath() t assertThat(commandErrorOutput.toString()).isEmpty(); } + @Test + public void blocksImport_rlpFormat() throws Exception { + final File fileToImport = temp.newFile("blocks.file"); + parseCommand( + BLOCK_SUBCOMMAND_NAME, + BLOCK_IMPORT_SUBCOMMAND_NAME, + "--format", + "RLP", + "--from", + fileToImport.getPath()); + + verify(mockBlockImporter).importBlockchain(pathArgumentCaptor.capture(), any()); + + assertThat(pathArgumentCaptor.getValue()).isEqualByComparingTo(fileToImport.toPath()); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void blocksImport_jsonFormat() throws Exception { + final String fileContent = "test"; + final File fileToImport = temp.newFile("blocks.file"); + final Writer fileWriter = Files.newBufferedWriter(fileToImport.toPath(), UTF_8); + fileWriter.write(fileContent); + fileWriter.close(); + + parseCommand( + BLOCK_SUBCOMMAND_NAME, + BLOCK_IMPORT_SUBCOMMAND_NAME, + "--format", + "JSON", + "--from", + fileToImport.getPath()); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + + verify(chainImporter, times(1)).importChain(stringArgumentCaptor.capture()); + assertThat(stringArgumentCaptor.getValue()).isEqualTo(fileContent); + } + // Export sub-sub-command @Test public void callingBlockExportSubCommandWithoutStartBlockMustDisplayErrorAndUsage() { diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-parent.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-parent.json new file mode 100644 index 0000000000..c584a79b82 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-parent.json @@ -0,0 +1,16 @@ +{ + "blocks": [ + { + "number": "0x0999", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-tx.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-tx.json new file mode 100644 index 0000000000..f7526c3a90 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-invalid-bad-tx.json @@ -0,0 +1,16 @@ +{ + "blocks": [ + { + "number": "0x01", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0x7FFFFFFFFFFFFFFF", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-special-fields.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-special-fields.json new file mode 100644 index 0000000000..2c3088e797 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-special-fields.json @@ -0,0 +1,17 @@ +{ + "blocks": [ + { + "extraData": "0x0123", + "coinbase": "627306090abaB3A6e1400e9345bC60c78a8BEf57", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-addendum.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-addendum.json new file mode 100644 index 0000000000..4e3b45ad3e --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-addendum.json @@ -0,0 +1,16 @@ +{ + "blocks": [ + { + "parentHash": "0x0", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-no-block-identifiers.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-no-block-identifiers.json new file mode 100644 index 0000000000..c57cd34344 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid-no-block-identifiers.json @@ -0,0 +1,44 @@ +{ + "blocks": [ + { + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + }, + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF2", + "gasPrice": "0xEF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "transactions": [] + }, + { + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "data": "0xc87509a1c067" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid.json new file mode 100644 index 0000000000..03a4937047 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/blocks-import-valid.json @@ -0,0 +1,48 @@ +{ + "blocks": [ + { + "number": 1, + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + }, + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF2", + "gasPrice": "0xEF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "number": 2, + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "number": 3, + "transactions": [] + }, + { + "number": 4, + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "data": "0xc87509a1c067" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/genesis.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/genesis.json new file mode 100644 index 0000000000..c46060ffd3 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/clique/genesis.json @@ -0,0 +1,45 @@ +{ + "config": { + "chainId": 2018, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "constantinopleFixBlock": 0, + "contractSizeLimit": 2147483647, + "clique": { + "blockperiodseconds": 15, + "epochlength": 30000 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-parent.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-parent.json new file mode 100644 index 0000000000..c584a79b82 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-parent.json @@ -0,0 +1,16 @@ +{ + "blocks": [ + { + "number": "0x0999", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-tx.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-tx.json new file mode 100644 index 0000000000..f7526c3a90 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-invalid-bad-tx.json @@ -0,0 +1,16 @@ +{ + "blocks": [ + { + "number": "0x01", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0x7FFFFFFFFFFFFFFF", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-special-fields.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-special-fields.json new file mode 100644 index 0000000000..2c3088e797 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-special-fields.json @@ -0,0 +1,17 @@ +{ + "blocks": [ + { + "extraData": "0x0123", + "coinbase": "627306090abaB3A6e1400e9345bC60c78a8BEf57", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-addendum.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-addendum.json new file mode 100644 index 0000000000..4e3b45ad3e --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-addendum.json @@ -0,0 +1,16 @@ +{ + "blocks": [ + { + "parentHash": "0x0", + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-no-block-identifiers.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-no-block-identifiers.json new file mode 100644 index 0000000000..c8f00b14cf --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid-no-block-identifiers.json @@ -0,0 +1,48 @@ +{ + "blocks": [ + { + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + }, + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF2", + "gasPrice": "0xEF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "coinbase": "0x02", + "extradata": "0x1234", + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "coinbase": "f17f52151EbEF6C7334FAD080c5704D77216b732", + "extradata": "0x3456", + "transactions": [] + }, + { + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "data": "0xc87509a1c067" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid.json new file mode 100644 index 0000000000..be1de8820d --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/blocks-import-valid.json @@ -0,0 +1,52 @@ +{ + "blocks": [ + { + "number": 1, + "transactions": [ + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF1", + "gasPrice": "0xFF", + "value": "0x01", + "to": "627306090abaB3A6e1400e9345bC60c78a8BEf57" + }, + { + "fromPrivateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "gasLimit": "0xFFFFF2", + "gasPrice": "0xEF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "number": 2, + "coinbase": "0x02", + "extradata": "0x1234", + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "to": "f17f52151EbEF6C7334FAD080c5704D77216b732" + } + ] + }, + { + "number": 3, + "coinbase": "f17f52151EbEF6C7334FAD080c5704D77216b732", + "extradata": "0x3456", + "transactions": [] + }, + { + "number": 4, + "transactions": [ + { + "fromPrivateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "gasLimit": "0xFFFFFF", + "gasPrice": "0xFF", + "data": "0xc87509a1c067" + } + ] + } + ] +} \ No newline at end of file diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/genesis.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/genesis.json new file mode 100644 index 0000000000..9c7ed48ad8 --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/ethash/genesis.json @@ -0,0 +1,44 @@ +{ + "config": { + "chainId": 2018, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "constantinopleFixBlock": 0, + "contractSizeLimit": 2147483647, + "ethash": { + "fixeddifficulty": 100 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/unsupported/genesis.json b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/unsupported/genesis.json new file mode 100644 index 0000000000..fd6b595d2d --- /dev/null +++ b/pantheon/src/test/resources/tech/pegasys/pantheon/chainimport/unsupported/genesis.json @@ -0,0 +1,46 @@ +{ + "config": { + "chainId": 2018, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "constantinopleFixBlock": 0, + "contractSizeLimit": 2147483647, + "ibft2": { + "blockperiodseconds": 1, + "epochlength": 30000, + "requesttimeoutseconds": 5 + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0xf83ea00000000000000000000000000000000000000000000000000000000000000000d594fe3b557e8fb62b89f4916b721be55ceb828dbd73808400000000c0", + "gasLimit": "0x1fffffffffffff", + "difficulty": "0x10000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +}